From 6043a4add558b143da9e3a3b37c758537f4b1064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:05:21 +0200 Subject: [PATCH] basic transpilation of template string working --- Cargo.toml | 3 +- src/base/source_file.rs | 6 + src/semantic/error.rs | 5 +- src/semantic/mod.rs | 26 +- src/syntax/syntax_tree/expression.rs | 8 + src/syntax/syntax_tree/statement.rs | 16 +- src/transpile/conversions.rs | 33 ++- src/transpile/error.rs | 5 +- src/transpile/expression.rs | 138 ++++++--- src/transpile/function.rs | 103 ++++--- src/transpile/internal_functions.rs | 4 +- src/transpile/transpiler.rs | 316 ++++++++++++++------ src/transpile/util.rs | 415 ++++++++++++++++++++------- src/transpile/variables.rs | 103 ++++--- 14 files changed, 841 insertions(+), 340 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d17efa9..a243bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,13 +35,14 @@ pathdiff = "0.2.3" serde = { version = "1.0.217", features = ["derive"], optional = true } serde_json = { version = "1.0.138", optional = true } # shulkerbox = { version = "0.1.0", default-features = false, optional = true } -shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "6ff544131b2518b8c92bc4d2de7682efd7141ec4", default-features = false, optional = true } +shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "89709834da6f39840caa9c6a2eadbdececdc7d44", default-features = false, optional = true } strsim = "0.11.1" strum = { version = "0.27.0", features = ["derive"] } thiserror = "2.0.11" tracing = "0.1.41" [dev-dependencies] +assert-struct = "0.2.0" serde_json = "1.0.138" [[example]] diff --git a/src/base/source_file.rs b/src/base/source_file.rs index 35852d7..4c5dab5 100644 --- a/src/base/source_file.rs +++ b/src/base/source_file.rs @@ -219,6 +219,12 @@ impl std::hash::Hash for Span { } } +impl SourceElement for Span { + fn span(&self) -> Span { + self.clone() + } +} + impl Span { /// Create a span from the given start and end byte indices in the source file. /// diff --git a/src/semantic/error.rs b/src/semantic/error.rs index 5aefacc..ee58aa8 100644 --- a/src/semantic/error.rs +++ b/src/semantic/error.rs @@ -81,7 +81,10 @@ impl Display for ConflictingFunctionNames { "{}", Message::new( Severity::Error, - format!("the following function declaration conflicts with an existing function with name `{}`", self.name) + format!( + "the following function declaration conflicts with an existing function with name `{}`", + self.name + ) ) )?; diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index 71e3369..e5fc7c1 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -173,13 +173,12 @@ impl Function { if let Some(incompatible) = self.annotations().iter().find(|a| { ["tick", "load", "uninstall"].contains(&a.assignment().identifier.span.str()) }) { - let err = - error::Error::IncompatibleFunctionAnnotation(IncompatibleFunctionAnnotation { - span: incompatible.assignment().identifier.span.clone(), - reason: - "functions with the `tick`, `load` or `uninstall` annotation cannot have parameters" - .to_string(), - }); + let err = error::Error::IncompatibleFunctionAnnotation(IncompatibleFunctionAnnotation { + span: incompatible.assignment().identifier.span.clone(), + reason: + "functions with the `tick`, `load` or `uninstall` annotation cannot have parameters" + .to_string(), + }); handler.receive(err.clone()); return Err(err); } else if parameters @@ -191,13 +190,12 @@ impl Function { .iter() .find(|a| a.assignment().identifier.span.str() == "deobfuscate") { - let err = - error::Error::IncompatibleFunctionAnnotation(IncompatibleFunctionAnnotation { - span: incompatible.assignment().identifier.span.clone(), - reason: - "functions with the `deobfuscate` annotation cannot have compile-time parameters" - .to_string(), - }); + let err = error::Error::IncompatibleFunctionAnnotation(IncompatibleFunctionAnnotation { + span: incompatible.assignment().identifier.span.clone(), + reason: + "functions with the `deobfuscate` annotation cannot have compile-time parameters" + .to_string(), + }); handler.receive(err.clone()); return Err(err); } diff --git a/src/syntax/syntax_tree/expression.rs b/src/syntax/syntax_tree/expression.rs index e833db1..af4c7fa 100644 --- a/src/syntax/syntax_tree/expression.rs +++ b/src/syntax/syntax_tree/expression.rs @@ -415,6 +415,14 @@ impl TemplateStringLiteral { pub fn parts(&self) -> &[TemplateStringLiteralPart] { &self.parts } + + /// Returns whether the template string literal contains any expressions. + #[must_use] + pub fn contains_expression(&self) -> bool { + self.parts + .iter() + .any(|part| matches!(part, TemplateStringLiteralPart::Expression { .. })) + } } impl SourceElement for TemplateStringLiteral { diff --git a/src/syntax/syntax_tree/statement.rs b/src/syntax/syntax_tree/statement.rs index c16ea1e..1e1dbd0 100644 --- a/src/syntax/syntax_tree/statement.rs +++ b/src/syntax/syntax_tree/statement.rs @@ -828,13 +828,15 @@ impl Parser<'_> { parser.parse_statement(handler).map_or_else( |_| { // error recovery - parser.stop_at(|reading| matches!( - reading, - Reading::Atomic(Token::Punctuation(punc)) if punc.punctuation == ';' - ) || matches!( - reading, - Reading::IntoDelimited(punc) if punc.punctuation == '{' - )); + parser.stop_at(|reading| { + matches!( + reading, + Reading::Atomic(Token::Punctuation(punc)) if punc.punctuation == ';' + ) || matches!( + reading, + Reading::IntoDelimited(punc) if punc.punctuation == '{' + ) + }); // goes after the semicolon or the open brace parser.forward(); diff --git a/src/transpile/conversions.rs b/src/transpile/conversions.rs index 6225952..3eb01e6 100644 --- a/src/transpile/conversions.rs +++ b/src/transpile/conversions.rs @@ -1,26 +1,37 @@ //! Conversion functions for converting between tokens/ast-nodes and [`shulkerbox`] types -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; -use shulkerbox::util::{MacroString as ExtMacroString, MacroStringPart as ExtMacroStringPart}; +use shulkerbox::{ + prelude::Command, + util::{MacroString as ExtMacroString, MacroStringPart as ExtMacroStringPart}, +}; use crate::{ - base::{self, Handler}, + base::{self, source_file::Span, Handler}, semantic::error::UnexpectedExpression, syntax::syntax_tree::expression::{TemplateStringLiteral, TemplateStringLiteralPart}, - transpile::{Scope, TranspileError, TranspileResult}, + transpile::{expression::DataLocation, Scope, TranspileError, TranspileResult}, util, }; use super::util::{MacroString, MacroStringPart}; -impl From for ExtMacroString { - fn from(value: MacroString) -> Self { - match value { - MacroString::String(s) => Self::String(s), - MacroString::MacroString(parts) => { - Self::MacroString(parts.into_iter().map(ExtMacroStringPart::from).collect()) - } +type ShulkerboxMacroStringMap = BTreeMap, Span)>; + +impl MacroString { + pub fn into_sb(self) -> (ExtMacroString, ShulkerboxMacroStringMap) { + match self { + Self::String(s) => (ExtMacroString::String(s), BTreeMap::new()), + Self::MacroString { + parts, + prepare_variables, + } => ( + ExtMacroString::MacroString( + parts.into_iter().map(ExtMacroStringPart::from).collect(), + ), + prepare_variables, + ), } } } diff --git a/src/transpile/error.rs b/src/transpile/error.rs index b232b49..3d6f402 100644 --- a/src/transpile/error.rs +++ b/src/transpile/error.rs @@ -465,10 +465,7 @@ impl Display for IllegalIndexingReason { write!(f, "The expression cannot be indexed.") } Self::InvalidComptimeType { expected } => { - write!( - f, - "The expression can only be indexed with type {expected} that can be evaluated at compile time." - ) + write!(f, "The expression can only be indexed with type {expected} that can be evaluated at compile time.") } Self::IndexOutOfBounds { index, length } => { write!( diff --git a/src/transpile/expression.rs b/src/transpile/expression.rs index ff72698..e6c69ea 100644 --- a/src/transpile/expression.rs +++ b/src/transpile/expression.rs @@ -1,8 +1,6 @@ //! The expression transpiler. -use std::{fmt::Display, string::ToString}; - -use super::util::MacroString; +use std::fmt::Display; #[cfg(feature = "shulkerbox")] use enum_as_inner::EnumAsInner; @@ -15,6 +13,7 @@ use super::{ error::{ IllegalIndexing, IllegalIndexingReason, MismatchedTypes, NotComptime, UnknownIdentifier, }, + util::MacroString, Scope, TranspileResult, Transpiler, VariableData, }; #[cfg(feature = "shulkerbox")] @@ -35,6 +34,7 @@ use crate::{ use std::sync::Arc; /// Compile-time evaluated value +#[cfg(feature = "shulkerbox")] #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum ComptimeValue { @@ -44,6 +44,7 @@ pub enum ComptimeValue { MacroString(MacroString), } +#[cfg(feature = "shulkerbox")] impl ComptimeValue { /// Returns the value as a string not containing a macro. #[must_use] @@ -152,7 +153,8 @@ impl From for ExpectedType { /// Location of data #[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum DataLocation { ScoreboardValue { objective: String, @@ -179,21 +181,34 @@ impl DataLocation { Self::Storage { r#type, .. } => match r#type { StorageType::Boolean => ValueType::Boolean, StorageType::Byte | StorageType::Int | StorageType::Long => ValueType::Integer, + StorageType::String => ValueType::String, StorageType::Double => todo!("Double storage type"), }, } } + + /// Returns the storage type of the data location. + #[must_use] + pub fn storage_type(&self) -> StorageType { + match self { + Self::ScoreboardValue { .. } => StorageType::Int, + Self::Tag { .. } => StorageType::Boolean, + Self::Storage { r#type, .. } => *r#type, + } + } } /// The type of a storage. #[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StorageType { Boolean, Byte, Int, Long, Double, + String, } impl StorageType { @@ -205,6 +220,7 @@ impl StorageType { Self::Int => "", Self::Long => "l", Self::Double => "d", + Self::String => "", } } @@ -216,6 +232,7 @@ impl StorageType { Self::Int => "int", Self::Long => "long", Self::Double => "double", + Self::String => "string", } } } @@ -413,15 +430,9 @@ impl Primary { }) .and_then(|val| val), Self::TemplateStringLiteral(template_string_literal) => { - use crate::syntax::syntax_tree::expression::TemplateStringLiteralPart; - - if template_string_literal - .parts() - .iter() - .any(|part| matches!(part, TemplateStringLiteralPart::Expression { .. })) - { + if template_string_literal.contains_expression() { template_string_literal - .to_macro_string(scope, handler) + .to_macro_string(None, scope, handler) .map(ComptimeValue::MacroString) .map_err(|_| NotComptime { expression: template_string_literal.span(), @@ -1310,7 +1321,9 @@ impl Transpiler { handler, )?; self.initialize_constant_score(-1); - let negate_cmd = Command::Raw(format!("scoreboard players operation {score_target} {objective} *= -1 shu_constants")); + let negate_cmd = Command::Raw(format!( + "scoreboard players operation {score_target} {objective} *= -1 shu_constants" + )); expr_cmds.push(negate_cmd); Ok(expr_cmds) @@ -1377,7 +1390,9 @@ impl Transpiler { let run_cmd = if run_cmds.len() == 1 { run_cmds.into_iter().next().expect("length is 1") } else { - Command::Group(run_cmds) + use shulkerbox::datapack::Group; + + Command::Group(Group::new(run_cmds)) }; match target { DataLocation::ScoreboardValue { objective, target } => { @@ -1416,12 +1431,18 @@ impl Transpiler { self.get_temp_storage_locations_array(); let store_cmd = Command::Execute(Execute::Store( - format!("success storage {temp_storage_name} {temp_storage_path} boolean 1.0").into(), - Box::new(Execute::Run(Box::new(run_cmd))) + format!( + "success storage {temp_storage_name} {temp_storage_path} boolean 1.0" + ) + .into(), + Box::new(Execute::Run(Box::new(run_cmd))), )); let if_cmd = Command::Execute(Execute::If( - Condition::Atom(format!("data storage {temp_storage_name} {{{temp_storage_name}:1b}}").into()), + Condition::Atom( + format!("data storage {temp_storage_name} {{{temp_storage_name}:1b}}") + .into(), + ), Box::new(Execute::Run(Box::new(success_cmd))), None, )); @@ -1676,12 +1697,15 @@ impl Transpiler { Vec::new(), ExtendedCondition::Runtime(Condition::Atom(s.str_content().to_string().into())), )), - Primary::TemplateStringLiteral(template_string) => Ok(( - Vec::new(), - ExtendedCondition::Runtime(Condition::Atom( - template_string.to_macro_string(scope, handler)?.into(), - )), - )), + Primary::TemplateStringLiteral(template_string) => { + let (macro_string, prepare_variables) = template_string + .to_macro_string(Some(self), scope, handler)? + .into_sb(); + Ok(( + Vec::new(), + ExtendedCondition::Runtime(Condition::Atom(macro_string)), + )) + } Primary::FunctionCall(func) => { if func .arguments() @@ -1858,13 +1882,16 @@ impl Transpiler { ComptimeValue::String(s) => Ok(( Vec::new(), ExtendedCondition::Runtime(Condition::Atom( - MacroString::String(s).into(), + shulkerbox::util::MacroString::String(s), )), )), - ComptimeValue::MacroString(s) => Ok(( - Vec::new(), - ExtendedCondition::Runtime(Condition::Atom(s.into())), - )), + ComptimeValue::MacroString(s) => { + let (macro_string, prepare_variables) = s.into_sb(); + Ok(( + Vec::new(), + ExtendedCondition::Runtime(Condition::Atom(macro_string)), + )) + } }, ), Primary::Prefix(prefix) => match prefix.operator() { @@ -1917,10 +1944,13 @@ impl Transpiler { Vec::new(), ExtendedCondition::Runtime(Condition::Atom(value.into())), )), - Ok(ComptimeValue::MacroString(value)) => Ok(( - Vec::new(), - ExtendedCondition::Runtime(Condition::Atom(value.into())), - )), + Ok(ComptimeValue::MacroString(value)) => { + let (macro_string, prepare_variables) = value.into_sb(); + Ok(( + Vec::new(), + ExtendedCondition::Runtime(Condition::Atom(macro_string)), + )) + } Ok(ComptimeValue::Boolean(boolean)) => { Ok((Vec::new(), ExtendedCondition::Comptime(boolean))) } @@ -2078,6 +2108,14 @@ impl Transpiler { handler.receive(Box::new(err.clone())); return Err(err); } + StorageType::String => { + let err = TranspileError::MismatchedTypes(MismatchedTypes { + expected_type: ExpectedType::String, + expression: binary.span(), + }); + handler.receive(Box::new(err.clone())); + return Err(err); + } }, }; @@ -2204,6 +2242,7 @@ impl Transpiler { } } + #[expect(clippy::too_many_lines)] fn store_comptime_value( &mut self, value: &ComptimeValue, @@ -2294,12 +2333,15 @@ impl Transpiler { r#type: StorageType::Boolean, .. } - | DataLocation::Tag { .. } => self.store_condition_success( - ExtendedCondition::Runtime(Condition::Atom(value.clone().into())), - target, - source, - handler, - ), + | DataLocation::Tag { .. } => { + let (macro_string, prepare_variables) = value.clone().into_sb(); + self.store_condition_success( + ExtendedCondition::Runtime(Condition::Atom(macro_string)), + target, + source, + handler, + ) + } // DataLocation::Storage { storage_name, path, r#type: StorageType::String } => todo!("implement storage string") _ => { let err = TranspileError::MismatchedTypes(MismatchedTypes { @@ -2370,6 +2412,12 @@ impl Transpiler { Ok(vec![cmd]) } + pub(super) fn get_temp_count(&mut self, amount: usize) -> usize { + let current = self.temp_counter; + self.temp_counter = self.temp_counter.wrapping_add(amount); + current + } + /// Get temporary scoreboard locations. pub(super) fn get_temp_scoreboard_locations(&mut self, amount: usize) -> (String, Vec) { let objective = "shu_temp_".to_string() @@ -2378,20 +2426,20 @@ impl Transpiler { self.datapack .register_scoreboard(&objective, None::<&str>, None::<&str>); + let temp_count = self.get_temp_count(amount); + let targets = (0..amount) .map(|i| { chksum_md5::hash(format!( "{namespace}\0{j}", namespace = self.main_namespace_name, - j = i + self.temp_counter + j = i + temp_count )) .to_hex_lowercase() .split_off(16) }) .collect(); - self.temp_counter = self.temp_counter.wrapping_add(amount); - (objective, targets) } @@ -2411,20 +2459,20 @@ impl Transpiler { let storage_name = "shulkerscript:temp_".to_string() + &chksum_md5::hash(&self.main_namespace_name).to_hex_lowercase(); + let temp_count = self.get_temp_count(amount); + let paths = (0..amount) .map(|i| { chksum_md5::hash(format!( "{namespace}\0{j}", namespace = self.main_namespace_name, - j = i + self.temp_counter + j = i + temp_count )) .to_hex_lowercase() .split_off(16) }) .collect::>(); - self.temp_counter = self.temp_counter.wrapping_add(amount); - self.temp_data_storage_locations.extend( paths .iter() diff --git a/src/transpile/function.rs b/src/transpile/function.rs index 70cd4b3..02e92cf 100644 --- a/src/transpile/function.rs +++ b/src/transpile/function.rs @@ -449,21 +449,29 @@ impl Transpiler { Ok(Parameter::Static(string.str_content().to_string().into())) } Expression::Primary(Primary::TemplateStringLiteral(literal)) => { - Ok(Parameter::Static(literal.to_macro_string(scope, handler)?)) + Ok(Parameter::Static(literal.to_macro_string( + Some(self), + scope, + handler, + )?)) } Expression::Primary(primary @ Primary::Identifier(ident)) => { let var = scope.get_variable(ident.span.str()).ok_or_else(|| { - let err = - TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(ident.span(), scope)); + let err = TranspileError::UnknownIdentifier( + UnknownIdentifier::from_scope(ident.span(), scope), + ); handler.receive(Box::new(err.clone())); err })?; match var.as_ref() { VariableData::MacroParameter { macro_name, .. } => { - Ok(Parameter::Static(MacroString::MacroString(vec![ - MacroStringPart::MacroUsage(macro_name.clone()), - ]))) + Ok(Parameter::Static(MacroString::MacroString { + parts: vec![MacroStringPart::MacroUsage( + macro_name.clone(), + )], + prepare_variables: BTreeMap::new(), + })) } VariableData::BooleanStorage { .. } @@ -605,7 +613,10 @@ impl Transpiler { crate::util::escape_str(&value).to_string(), ), ), - MacroString::MacroString(parts) => { + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } => { let parts = parts .into_iter() .map(|part| match part { @@ -622,7 +633,10 @@ impl Transpiler { .collect(); statics.insert( arg_name.to_string(), - MacroString::MacroString(parts), + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + }, ) } }; @@ -634,9 +648,9 @@ impl Transpiler { } => { require_dyn_params = true; setup_cmds.extend(prepare_cmds); - move_cmds.push(Command::Raw( - format!(r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}") - )); + move_cmds.push(Command::Raw(format!( + r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}" + ))); } } } @@ -654,7 +668,9 @@ impl Transpiler { Parameter::Static(s) => match s.as_str() { Ok(s) => { if s.parse::().is_ok() { - move_cmds.push(Command::Raw(format!(r"scoreboard players set {target} {objective} {s}"))); + move_cmds.push(Command::Raw(format!( + r"scoreboard players set {target} {objective} {s}" + ))); } else { let err = TranspileError::MismatchedTypes( MismatchedTypes { @@ -666,11 +682,19 @@ impl Transpiler { return Err(err); } } - Err(parts) => { - move_cmds.push(Command::UsesMacro(MacroString::MacroString( - std::iter::once(MacroStringPart::String(format!("scoreboard players set {target} {objective} "))) - .chain(parts.iter().cloned()).collect() - ).into())); + Err((parts, preparation_variables)) => { + let (macro_string, preparation_variables) = MacroString::MacroString { + parts: std::iter::once(MacroStringPart::String( + format!( + "scoreboard players set {target} {objective} " + ), + )) + .chain(parts.iter().cloned()) + .collect(), + prepare_variables: preparation_variables.to_owned(), + } + .into_sb(); + move_cmds.push(Command::UsesMacro(macro_string)); } }, Parameter::Storage { @@ -703,7 +727,10 @@ impl Transpiler { Parameter::Static(s) => match s.as_str() { Ok(s) => { if let Ok(b) = s.parse::() { - move_cmds.push(Command::Raw(format!("data modify storage {target_storage_name} {target_path} set value {}", if b { "1b" } else { "0b" }))); + move_cmds.push(Command::Raw(format!( + "data modify storage {target_storage_name} {target_path} set value {}", + if b { "1b" } else { "0b" } + ))); } else { let err = TranspileError::MismatchedTypes( MismatchedTypes { @@ -715,11 +742,15 @@ impl Transpiler { return Err(err); } } - Err(parts) => { - move_cmds.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())); + Err((parts, preparation_cmds)) => { + let (macro_string, preparation_variables) = MacroString::MacroString { + parts: std::iter::once(MacroStringPart::String(format!("data modify storage {target_storage_name} {target_path} set value "))) + .chain(parts.iter().cloned()) + .collect(), + prepare_variables: preparation_cmds.to_owned(), + } + .into_sb(); + move_cmds.push(Command::UsesMacro(macro_string)); } }, Parameter::Storage { @@ -756,7 +787,10 @@ impl Transpiler { } MacroString::String(s) } - MacroString::MacroString(mut parts) => { + MacroString::MacroString { + mut parts, + prepare_variables: preparation_cmds, + } => { parts.insert( 0, MacroStringPart::String(format!(r#"{k}:""#)), @@ -766,7 +800,10 @@ impl Transpiler { ending.push(','); } parts.push(MacroStringPart::String(ending)); - MacroString::MacroString(parts) + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } } }), ); @@ -775,18 +812,18 @@ impl Transpiler { MacroString::String(s) => Command::Raw(format!( r"data merge storage shulkerscript:function_arguments_{storage_suffix} {{{s}}}" )), - MacroString::MacroString(_) => { - let prefix = MacroString::String( - format!("data merge storage shulkerscript:function_arguments_{storage_suffix} {{"), - ); - Command::UsesMacro( + MacroString::MacroString { .. } => { + let prefix = MacroString::String(format!( + "data merge storage shulkerscript:function_arguments_{storage_suffix} {{" + )); + let (macro_string, prepare_variables) = super::util::join_macro_strings([ prefix, joined_statics, MacroString::String("}".to_string()), ]) - .into(), - ) + .into_sb(); + Command::UsesMacro(macro_string) } }; setup_cmds.push(statics_cmd); @@ -832,7 +869,7 @@ fn comptime_args_hash(args: &[Option]) -> String { ComptimeValue::String(s) => Cow::Borrowed(s.as_str()), ComptimeValue::MacroString(s) => match s.as_str() { Ok(s) => s, - Err(parts) => { + Err((parts, _)) => { let s = parts .iter() .map(|p| match p { diff --git a/src/transpile/internal_functions.rs b/src/transpile/internal_functions.rs index 449259c..d107c36 100644 --- a/src/transpile/internal_functions.rs +++ b/src/transpile/internal_functions.rs @@ -589,7 +589,9 @@ fn print_function( let cmd = format!("tellraw {target} {print_args}"); let cmd = if contains_macro { - Command::UsesMacro(cmd.parse::().expect("cannot fail").into()) + let (macro_string, prepare_variables) = + cmd.parse::().expect("cannot fail").into_sb(); + Command::UsesMacro(macro_string) } else { Command::Raw(cmd) }; diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index 6aa5a36..19d2ab7 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -7,10 +7,14 @@ use std::{ }; use itertools::Itertools; -use shulkerbox::datapack::{self, Command, Datapack, Execute}; +use shulkerbox::datapack::{self, Command, Datapack, Execute, Group}; use crate::{ - base::{self, source_file::SourceElement, Handler}, + base::{ + self, + source_file::{SourceElement, Span}, + Handler, + }, semantic::error::UnexpectedExpression, syntax::syntax_tree::{ declaration::{Declaration, FunctionVariableType, ImportItems}, @@ -24,6 +28,7 @@ use crate::{ }, transpile::{ error::IllegalAnnotationContent, + expression::DataLocation, util::{MacroString, MacroStringPart}, variables::FunctionVariableDataType, }, @@ -411,7 +416,7 @@ impl Transpiler { if commands.is_empty() { Ok(Vec::new()) } else { - Ok(vec![Command::Group(commands)]) + Ok(vec![Command::Group(Group::new(commands))]) } } Statement::Semicolon(semi) => match semi.statement() { @@ -467,7 +472,8 @@ impl Transpiler { .map(|val| val.to_macro_string()); let (prepare_cmds, ret_cmd) = if let Ok(val) = comptime_val { - (Vec::new(), datapack::ReturnCommand::Value(val.into())) + let (macro_string, prepare_variables) = val.into_sb(); + (Vec::new(), datapack::ReturnCommand::Value(macro_string)) } else { match ret.expression() { Expression::Primary(Primary::Prefix(prefix)) @@ -478,7 +484,7 @@ impl Transpiler { let cmd = if ret_cmds.len() == 1 { ret_cmds.into_iter().next().unwrap() } else { - Command::Group(ret_cmds) + Command::Group(Group::new(ret_cmds)) }; (Vec::new(), datapack::ReturnCommand::Command(Box::new(cmd))) } @@ -487,7 +493,7 @@ impl Transpiler { let cmd = if ret_cmds.len() == 1 { ret_cmds.into_iter().next().unwrap() } else { - Command::Group(ret_cmds) + Command::Group(Group::new(ret_cmds)) }; (Vec::new(), datapack::ReturnCommand::Command(Box::new(cmd))) } @@ -513,7 +519,11 @@ impl Transpiler { }, |val| { let cmd = val.to_string_no_macro().map_or_else( - || Command::UsesMacro(val.to_macro_string().into()), + || { + let (macro_string, prepare_variables) = + val.to_macro_string().into_sb(); + Command::UsesMacro(macro_string) + }, Command::Raw, ); Ok(( @@ -588,6 +598,7 @@ impl Transpiler { Ok(cmds) } + #[expect(clippy::too_many_lines)] pub(super) fn transpile_run_expression( &mut self, expression: &Primary, @@ -600,22 +611,29 @@ impl Transpiler { Some(VariableData::ComptimeValue { value, read_only: _, - }) => value.read().unwrap().as_ref().map_or_else( - || { + }) => { + if let Some(val) = value.read().unwrap().as_ref() { + let cmds = val.to_string_no_macro().map_or_else( + || { + let (macro_string, prepare_variables) = + val.to_macro_string().into_sb(); + self.transpile_commands_with_variable_macros( + vec![Command::UsesMacro(macro_string)], + prepare_variables, + handler, + ) + }, + |s| Ok(vec![Command::Raw(s)]), + )?; + Ok(cmds) + } else { let err = TranspileError::MissingValue(MissingValue { expression: ident.span.clone(), }); handler.receive(Box::new(err.clone())); Err(err) - }, - |val| { - let cmd = val.to_string_no_macro().map_or_else( - || Command::UsesMacro(val.to_macro_string().into()), - Command::Raw, - ); - Ok(vec![cmd]) - }, - ), + } + } Some(_) => { let err = TranspileError::UnexpectedExpression(UnexpectedExpression(Box::new( Expression::Primary(expression.clone()), @@ -647,12 +665,26 @@ impl Transpiler { Primary::StringLiteral(string) => { Ok(vec![Command::Raw(string.str_content().to_string())]) } - Primary::TemplateStringLiteral(string) => Ok(vec![Command::UsesMacro( - string.to_macro_string(scope, handler)?.into(), - )]), + Primary::TemplateStringLiteral(string) => { + let (macro_string, prepare_variables) = string + .to_macro_string(Some(self), scope, handler)? + .into_sb(); + self.transpile_commands_with_variable_macros( + vec![Command::UsesMacro(macro_string)], + prepare_variables, + handler, + ) + } Primary::Lua(code) => match code.eval_comptime(scope, handler)? { Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]), - Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]), + Ok(ComptimeValue::MacroString(cmd)) => { + let (macro_string, prepare_variables) = cmd.into_sb(); + self.transpile_commands_with_variable_macros( + vec![Command::UsesMacro(macro_string)], + prepare_variables, + handler, + ) + } Ok(ComptimeValue::Boolean(_) | ComptimeValue::Integer(_)) => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expected_type: ExpectedType::String, @@ -676,7 +708,14 @@ impl Transpiler { } Expression::Binary(bin) => match bin.comptime_eval(scope, handler) { Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]), - Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]), + Ok(ComptimeValue::MacroString(cmd)) => { + let (macro_string, prepare_variables) = cmd.into_sb(); + self.transpile_commands_with_variable_macros( + vec![Command::UsesMacro(macro_string)], + prepare_variables, + handler, + ) + } Ok(_) => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expression: bin.span(), @@ -723,47 +762,67 @@ impl Transpiler { TranspiledFunctionArguments::Static(arguments, mut setup_cmds) => { use std::fmt::Write; - let cmd = if arguments.is_empty() { - Command::Raw(function_call) + let cmds = if arguments.is_empty() { + vec![Command::Raw(function_call)] } else { - let arguments_iter = arguments.iter().map(|(ident, v)| match v { - MacroString::String(s) => MacroString::String(format!( - r#"{macro_name}:"{escaped}""#, - macro_name = crate::util::identifier_to_macro(ident), - escaped = crate::util::escape_str(s) - )), - MacroString::MacroString(parts) => MacroString::MacroString( - std::iter::once(MacroStringPart::String(format!( - r#"{macro_name}:""#, - macro_name = crate::util::identifier_to_macro(ident) - ))) - .chain(parts.clone().into_iter().map(|part| match part { - MacroStringPart::String(s) => MacroStringPart::String( - crate::util::escape_str(&s).to_string(), - ), - macro_usage @ MacroStringPart::MacroUsage(_) => macro_usage, - })) - .chain(std::iter::once(MacroStringPart::String('"'.to_string()))) - .collect(), - ), - }); + let arguments_iter = + arguments + .into_iter() + .map(|(macro_name, value)| match value { + MacroString::String(s) => MacroString::String(format!( + r#"{macro_name}:"{escaped}""#, + escaped = crate::util::escape_str(&s) + )), + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } => MacroString::MacroString { + parts: std::iter::once(MacroStringPart::String(format!( + r#"{macro_name}:""#, + ))) + .chain(parts.into_iter().map(|part| match part { + MacroStringPart::String(s) => MacroStringPart::String( + crate::util::escape_str(&s).to_string(), + ), + MacroStringPart::MacroUsage(_) => part, + })) + .chain(std::iter::once(MacroStringPart::String( + '"'.to_string(), + ))) + .collect(), + prepare_variables: preparation_cmds, + }, + }); let arguments = super::util::join_macro_strings(arguments_iter); match arguments { MacroString::String(arguments) => { write!(function_call, " {{{arguments}}}").unwrap(); - Command::Raw(function_call) + vec![Command::Raw(function_call)] } - MacroString::MacroString(mut parts) => { + MacroString::MacroString { + mut parts, + prepare_variables, + } => { function_call.push_str(" {"); parts.insert(0, MacroStringPart::String(function_call)); parts.push(MacroStringPart::String('}'.to_string())); - Command::UsesMacro(MacroString::MacroString(parts).into()) + + let (macro_string, prepare_variables) = MacroString::MacroString { + parts, + prepare_variables, + } + .into_sb(); + self.transpile_commands_with_variable_macros( + vec![Command::UsesMacro(macro_string)], + prepare_variables, + handler, + )? } } }; - setup_cmds.push(cmd); + setup_cmds.extend(cmds); Ok(setup_cmds) } @@ -963,6 +1022,7 @@ impl Transpiler { } } + #[expect(clippy::too_many_lines)] fn combine_execute_head_tail( &mut self, head: &ExecuteBlockHead, @@ -991,84 +1051,158 @@ impl Transpiler { } } ExecuteBlockHead::As(r#as) => { - let selector = r#as.as_selector().to_macro_string(scope, handler)?; - tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::As(selector.into(), Box::new(tail))) - }) + let selector = r#as + .as_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = selector.into_sb(); + tail.map(|(pre_cmds, tail)| (pre_cmds, Execute::As(macro_string, Box::new(tail)))) } ExecuteBlockHead::At(at) => { - let selector = at.at_selector().to_macro_string(scope, handler)?; - tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::At(selector.into(), Box::new(tail))) - }) + let selector = at + .at_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = selector.into_sb(); + tail.map(|(pre_cmds, tail)| (pre_cmds, Execute::At(macro_string, Box::new(tail)))) } ExecuteBlockHead::Align(align) => { - let align = align.align_selector().to_macro_string(scope, handler)?; + let align = align + .align_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = align.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Align(align.into(), Box::new(tail))) + (pre_cmds, Execute::Align(macro_string, Box::new(tail))) }) } ExecuteBlockHead::Anchored(anchored) => { - let anchor = anchored - .anchored_selector() - .to_macro_string(scope, handler)?; + let anchor = + anchored + .anchored_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = anchor.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Anchored(anchor.into(), Box::new(tail))) + (pre_cmds, Execute::Anchored(macro_string, Box::new(tail))) }) } ExecuteBlockHead::In(r#in) => { - let dimension = r#in.in_selector().to_macro_string(scope, handler)?; - tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::In(dimension.into(), Box::new(tail))) - }) + let dimension = r#in + .in_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = dimension.into_sb(); + tail.map(|(pre_cmds, tail)| (pre_cmds, Execute::In(macro_string, Box::new(tail)))) } ExecuteBlockHead::Positioned(positioned) => { - let position = positioned - .positioned_selector() - .to_macro_string(scope, handler)?; + let position = + positioned + .positioned_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = position.into_sb(); tail.map(|(pre_cmds, tail)| { - ( - pre_cmds, - Execute::Positioned(position.into(), Box::new(tail)), - ) + (pre_cmds, Execute::Positioned(macro_string, Box::new(tail))) }) } ExecuteBlockHead::Rotated(rotated) => { - let rotation = rotated.rotated_selector().to_macro_string(scope, handler)?; + let rotation = + rotated + .rotated_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = rotation.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Rotated(rotation.into(), Box::new(tail))) + (pre_cmds, Execute::Rotated(macro_string, Box::new(tail))) }) } ExecuteBlockHead::Facing(facing) => { - let facing = facing.facing_selector().to_macro_string(scope, handler)?; + let facing = + facing + .facing_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = facing.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Facing(facing.into(), Box::new(tail))) + (pre_cmds, Execute::Facing(macro_string, Box::new(tail))) }) } ExecuteBlockHead::AsAt(as_at) => { - let selector = as_at.asat_selector().to_macro_string(scope, handler)?; - tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::AsAt(selector.into(), Box::new(tail))) - }) + let selector = as_at + .asat_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = selector.into_sb(); + tail.map(|(pre_cmds, tail)| (pre_cmds, Execute::AsAt(macro_string, Box::new(tail)))) } ExecuteBlockHead::On(on) => { - let dimension = on.on_selector().to_macro_string(scope, handler)?; - tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::On(dimension.into(), Box::new(tail))) - }) + let dimension = on + .on_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = dimension.into_sb(); + tail.map(|(pre_cmds, tail)| (pre_cmds, Execute::On(macro_string, Box::new(tail)))) } ExecuteBlockHead::Store(store) => { - let store = store.store_selector().to_macro_string(scope, handler)?; + let store = store + .store_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = store.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Store(store.into(), Box::new(tail))) + (pre_cmds, Execute::Store(macro_string, Box::new(tail))) }) } ExecuteBlockHead::Summon(summon) => { - let entity = summon.summon_selector().to_macro_string(scope, handler)?; + let entity = + summon + .summon_selector() + .to_macro_string(Some(self), scope, handler)?; + let (macro_string, prepare_variables) = entity.into_sb(); tail.map(|(pre_cmds, tail)| { - (pre_cmds, Execute::Summon(entity.into(), Box::new(tail))) + (pre_cmds, Execute::Summon(macro_string, Box::new(tail))) }) } }) } + + pub(crate) fn transpile_commands_with_variable_macros( + &mut self, + cmds: Vec, + prepare_variables: BTreeMap, Span)>, + handler: &impl Handler, + ) -> TranspileResult> { + let storage_name = { + let temp = self.get_temp_count(1); + let hash = chksum_md5::hash(temp.to_le_bytes()); + format!("shulkerscript:arguments_{hash}") + }; + + let (macro_names, mut prepare_cmds, move_cmds) = prepare_variables.into_iter().try_fold( + (HashSet::new(), Vec::new(), Vec::new()), + |(mut vars, mut prepare_cmds, mut move_cmds), + (macro_name, (data_location, var_cmds, span))| { + vars.insert(macro_name.clone()); + prepare_cmds.extend(var_cmds); + let cur_move_cmds = self.move_data( + &data_location, + &DataLocation::Storage { + storage_name: storage_name.clone(), + path: macro_name, + r#type: data_location.storage_type(), + }, + &span, + handler, + )?; + move_cmds.extend(cur_move_cmds); + + TranspileResult::Ok((vars, prepare_cmds, move_cmds)) + }, + )?; + + prepare_cmds.extend(move_cmds); + + if prepare_cmds.is_empty() && macro_names.is_empty() { + Ok(cmds) + } else { + prepare_cmds.push(Command::Group( + Group::new(cmds) + .always_create_function(true) + .block_pass_macros(macro_names) + .data_storage_name(storage_name), + )); + + Ok(prepare_cmds) + } + } } diff --git a/src/transpile/util.rs b/src/transpile/util.rs index 2318e76..378715c 100644 --- a/src/transpile/util.rs +++ b/src/transpile/util.rs @@ -1,34 +1,48 @@ //! Utility methods for transpiling -use std::{fmt::Display, str::FromStr}; +#[cfg(feature = "shulkerbox")] +use std::{collections::BTreeMap, sync::Arc}; + +#[cfg(feature = "shulkerbox")] +use shulkerbox::prelude::Command; #[cfg(feature = "shulkerbox")] use crate::{ - base::{self, source_file::SourceElement as _, Handler}, + base::{ + self, + source_file::{SourceElement as _, Span}, + Handler, + }, syntax::syntax_tree::{ expression::{Expression, Primary, TemplateStringLiteral, TemplateStringLiteralPart}, AnyStringLiteral, }, transpile::{ error::{TranspileError, UnknownIdentifier}, - expression::ComptimeValue, - Scope, TranspileResult, VariableData, + expression::{ComptimeValue, DataLocation}, + Scope, TranspileResult, Transpiler, VariableData, }, + util::identifier_to_macro, }; -#[cfg(feature = "shulkerbox")] -use std::sync::Arc; /// String that can contain macros +#[cfg(feature = "shulkerbox")] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MacroString { /// A normal string String(String), /// A string containing expressions - MacroString(Vec), + MacroString { + /// Parts that make up the macro string + parts: Vec, + /// Variables that need special preparation before using the macro string + prepare_variables: BTreeMap, Span)>, + }, } /// Part of a [`MacroString`] +#[cfg(feature = "shulkerbox")] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MacroStringPart { @@ -38,11 +52,12 @@ pub enum MacroStringPart { MacroUsage(String), } -impl Display for MacroString { +#[cfg(feature = "shulkerbox")] +impl std::fmt::Display for MacroString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::String(s) => s.fmt(f), - Self::MacroString(parts) => { + Self::MacroString { parts, .. } => { for part in parts { match part { MacroStringPart::String(s) => s.fmt(f)?, @@ -55,13 +70,14 @@ impl Display for MacroString { } } +#[cfg(feature = "shulkerbox")] 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 + Self::MacroString { parts, .. } => parts .iter() .any(|p| matches!(p, MacroStringPart::MacroUsage(_))), } @@ -71,11 +87,23 @@ impl MacroString { /// /// # Errors /// - If the macro string contains macros - pub fn as_str(&self) -> Result, &[MacroStringPart]> { + #[expect(clippy::type_complexity)] + pub fn as_str( + &self, + ) -> Result< + std::borrow::Cow<'_, str>, + ( + &[MacroStringPart], + &BTreeMap, Span)>, + ), + > { 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( + Self::MacroString { + parts, + prepare_variables, + } if self.contains_macros() => Err((parts, prepare_variables)), + Self::MacroString { parts, .. } => Ok(std::borrow::Cow::Owned( parts .iter() .map(|p| match p { @@ -126,6 +154,7 @@ where } /// Join multiple macro strings into one +#[cfg(feature = "shulkerbox")] #[must_use] pub fn join_macro_strings(strings: I) -> MacroString where @@ -139,20 +168,39 @@ where s.push_str(&cur); MacroString::String(s) } - MacroString::MacroString(cur) => { + MacroString::MacroString { + parts: cur, + prepare_variables: preparation_cmds, + } => { let mut parts = vec![MacroStringPart::String(s)]; parts.extend(cur); - MacroString::MacroString(parts) + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } } }, - MacroString::MacroString(mut parts) => match cur { + MacroString::MacroString { + mut parts, + prepare_variables: mut preparation_cmds, + } => match cur { MacroString::String(cur) => { parts.push(MacroStringPart::String(cur)); - MacroString::MacroString(parts) + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } } - MacroString::MacroString(cur) => { + MacroString::MacroString { + parts: cur, + prepare_variables: cur_preparation_cmds, + } => { parts.extend(cur); - MacroString::MacroString(parts) + preparation_cmds.extend(cur_preparation_cmds); + MacroString::MacroString { + parts, + prepare_variables: preparation_cmds, + } } }, }) @@ -173,7 +221,8 @@ pub fn add_to_entity_selector(selector: impl Into, additional: &str) -> } } -impl FromStr for MacroString { +#[cfg(feature = "shulkerbox")] +impl std::str::FromStr for MacroString { type Err = (); fn from_str(s: &str) -> Result { @@ -239,7 +288,10 @@ impl FromStr for MacroString { .iter() .any(|p| matches!(p, MacroStringPart::MacroUsage(_))) { - Ok(Self::MacroString(parts)) + Ok(Self::MacroString { + parts, + prepare_variables: BTreeMap::new(), + }) } else { Ok(Self::String(s.to_string())) } @@ -249,6 +301,7 @@ impl FromStr for MacroString { } } +#[cfg(feature = "shulkerbox")] impl From for MacroString where S: Into, @@ -266,12 +319,15 @@ impl AnyStringLiteral { /// - If an identifier in a template string is not found in the scope pub fn to_macro_string( &self, + transpiler: Option<&mut Transpiler>, scope: &Arc, handler: &impl Handler, ) -> TranspileResult { match self { Self::StringLiteral(literal) => Ok(MacroString::from(literal.str_content().as_ref())), - Self::TemplateStringLiteral(literal) => literal.to_macro_string(scope, handler), + Self::TemplateStringLiteral(literal) => { + literal.to_macro_string(transpiler, scope, handler) + } } } } @@ -282,70 +338,215 @@ impl TemplateStringLiteral { /// /// # Errors /// - If an identifier in a template string is not found in the scope + #[expect(clippy::too_many_lines)] pub fn to_macro_string( &self, + mut transpiler: Option<&mut Transpiler>, scope: &Arc, handler: &impl Handler, ) -> TranspileResult { - if self - .parts() - .iter() - .any(|p| matches!(p, TemplateStringLiteralPart::Expression { .. })) - { - let macro_string = MacroString::MacroString( - self.parts() - .iter() - .map(|part| match part { - TemplateStringLiteralPart::Text(text) => Ok(MacroStringPart::String( - crate::util::unescape_template_string(text.span.str()).into_owned(), - )), - TemplateStringLiteralPart::Expression { expression, .. } => { - match expression.as_ref() { - Expression::Primary(Primary::Identifier(identifier)) => - { - #[expect(clippy::option_if_let_else)] - if let Some(var_data) = - scope.get_variable(identifier.span.str()) - { - match var_data.as_ref() { - VariableData::MacroParameter { macro_name, .. } => Ok( - MacroStringPart::MacroUsage(macro_name.to_owned()), - ), - VariableData::ComptimeValue { value, .. } => { - let value = value.read().unwrap().as_ref().map_or_else( - || "null".into(), - ComptimeValue::to_macro_string, - ); + if self.contains_expression() { + let mut prepare_variables = BTreeMap::new(); - match value.as_str() { - Ok(s) => Ok(MacroStringPart::String(s.into_owned())), - Err(_) => todo!("comptime value resulting in macro string with macros") - } + let parts = self + .parts() + .iter() + .map(|part| match part { + TemplateStringLiteralPart::Text(text) => Ok(vec![MacroStringPart::String( + crate::util::unescape_template_string(text.span.str()).into_owned(), + )]), + TemplateStringLiteralPart::Expression { expression, .. } => match expression + .as_ref() + { + Expression::Primary(Primary::Identifier(identifier)) => { + #[expect(clippy::option_if_let_else)] + if let Some(var_data) = scope.get_variable(identifier.span.str()) { + match var_data.as_ref() { + VariableData::MacroParameter { macro_name, .. } => { + Ok(vec![MacroStringPart::MacroUsage(macro_name.to_owned())]) + } + VariableData::ComptimeValue { value, .. } => { + let value = value.read().unwrap().as_ref().map_or_else( + || "null".into(), + ComptimeValue::to_macro_string, + ); + + match value.as_str() { + Ok(s) => { + Ok(vec![MacroStringPart::String(s.into_owned())]) + } + Err((inner_parts, inner_prepare_variables)) => { + prepare_variables + .extend(inner_prepare_variables.to_owned()); + Ok(inner_parts.to_vec()) } - _ => todo!("other identifiers in template strings"), } - } else { - let err = - TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(identifier.span(), scope)); + } + VariableData::BooleanStorage { storage_name, path } => { + use crate::transpile::expression::StorageType; + + let macro_name = if let Some(transpiler) = &mut transpiler { + let temp_count = transpiler.get_temp_count(1); + format!( + "shu_temp_{hash}", + hash = chksum_md5::hash(temp_count.to_le_bytes()) + ) + } else { + identifier_to_macro(identifier.span.str()).into_owned() + }; + + let data_location = DataLocation::Storage { + storage_name: storage_name.to_owned(), + path: path.to_owned(), + r#type: StorageType::Boolean, + }; + prepare_variables.insert( + macro_name.clone(), + (data_location, Vec::new(), expression.span()), + ); + + Ok(vec![MacroStringPart::MacroUsage(macro_name)]) + } + VariableData::ScoreboardValue { objective, target } => { + let macro_name = if let Some(transpiler) = &mut transpiler { + let temp_count = transpiler.get_temp_count(1); + format!( + "shu_temp_{hash}", + hash = chksum_md5::hash(temp_count.to_le_bytes()) + ) + } else { + identifier_to_macro(identifier.span.str()).into_owned() + }; + let data_location = DataLocation::ScoreboardValue { + objective: objective.to_owned(), + target: target.to_owned(), + }; + prepare_variables.insert( + macro_name.clone(), + (data_location, Vec::new(), expression.span()), + ); + + Ok(vec![MacroStringPart::MacroUsage(macro_name)]) + } + _ => { + use crate::semantic::error::UnexpectedExpression; + + let err = TranspileError::UnexpectedExpression( + UnexpectedExpression(expression.to_owned()), + ); handler.receive(Box::new(err.clone())); + Err(err) } } - Expression::Primary(Primary::MemberAccess(member_access)) => { - let value = member_access.parent().comptime_member_access(member_access, scope, handler).inspect_err(|err| { - handler.receive(Box::new(TranspileError::NotComptime(err.clone()))); - })?.to_macro_string(); - - value.as_str().map_or_else(|_| todo!("comptime value resulting in macro string with macros"), |s| Ok(MacroStringPart::String(s.into_owned()))) - } - _ => todo!("other expressions in template strings"), + } else { + let err = TranspileError::UnknownIdentifier( + UnknownIdentifier::from_scope(identifier.span(), scope), + ); + handler.receive(Box::new(err.clone())); + Err(err) } } - }) - .collect::>>()?, - ); + Expression::Primary(Primary::MemberAccess(member_access)) => { + let value = member_access + .parent() + .comptime_member_access(member_access, scope, handler) + .inspect_err(|err| { + handler.receive(Box::new(TranspileError::NotComptime( + err.clone(), + ))); + })? + .to_macro_string(); - Ok(macro_string) + match value.as_str() { + Ok(s) => Ok(vec![MacroStringPart::String(s.into_owned())]), + Err((inner_parts, inner_prepare_variables)) => { + prepare_variables.extend(inner_prepare_variables.to_owned()); + Ok(inner_parts.to_vec()) + } + } + } + _ => { + if let Some(transpiler) = &mut transpiler { + use crate::transpile::expression::{StorageType, ValueType}; + + let temp_count = transpiler.get_temp_count(1); + let macro_name = format!( + "shu_temp_{hash}", + hash = chksum_md5::hash(temp_count.to_le_bytes()) + ); + + let data_location = + if expression.can_yield_type(ValueType::Integer, scope) { + let (scoreboard_name, [scoreboard_target]) = + transpiler.get_temp_scoreboard_locations_array(); + + DataLocation::ScoreboardValue { + objective: scoreboard_name, + target: scoreboard_target, + } + } else if expression.can_yield_type(ValueType::Boolean, scope) { + let (storage_name, [storage_path]) = + transpiler.get_temp_storage_locations_array(); + DataLocation::Storage { + storage_name, + path: storage_path, + r#type: StorageType::Boolean, + } + } else if expression.can_yield_type(ValueType::String, scope) { + let (storage_name, [storage_path]) = + transpiler.get_temp_storage_locations_array(); + DataLocation::Storage { + storage_name, + path: storage_path, + r#type: StorageType::String, + } + } else { + use crate::semantic::error::UnexpectedExpression; + + let err = TranspileError::UnexpectedExpression( + UnexpectedExpression(expression.to_owned()), + ); + handler.receive(Box::new(err.clone())); + return Err(err); + }; + + let commands = transpiler.transpile_expression( + expression, + &data_location, + scope, + handler, + )?; + + prepare_variables.insert( + macro_name.clone(), + (data_location, commands, expression.span()), + ); + + Ok(vec![MacroStringPart::MacroUsage(macro_name)]) + } else { + use crate::semantic::error::UnexpectedExpression; + + let err = TranspileError::UnexpectedExpression( + UnexpectedExpression(expression.to_owned()), + ); + handler.receive(Box::new(err.clone())); + + Err(err) + } + } + }, + }) + .flat_map(|res| match res { + Ok(parts) => parts.into_iter().map(Ok).collect(), + Err(err) => vec![Err(err)], + }) + .collect::>>()?; + + Ok(MacroString::MacroString { + parts, + prepare_variables, + }) } else { Ok(MacroString::String( self.as_str(scope, handler)?.into_owned(), @@ -354,46 +555,62 @@ impl TemplateStringLiteral { } } -#[cfg(test)] +#[cfg(all(test, feature = "shulkerbox"))] mod tests { + use std::str::FromStr as _; + + use assert_struct::assert_struct; + use super::*; #[test] fn test_parse_macro_string() { - assert_eq!( + assert_struct!( MacroString::from_str("Hello, $(world)!").unwrap(), - MacroString::MacroString(vec![ - MacroStringPart::String("Hello, ".to_string()), - MacroStringPart::MacroUsage("world".to_string()), - MacroStringPart::String("!".to_string()) - ]) + MacroString::MacroString { + parts: vec![ + MacroStringPart::String("Hello, ".to_string()), + MacroStringPart::MacroUsage("world".to_string()), + MacroStringPart::String("!".to_string()) + ], + prepare_variables.is_empty(): true, + } ); - assert_eq!( + assert_struct!( MacroString::from_str("Hello, $(world)! $(world").unwrap(), - MacroString::MacroString(vec![ - MacroStringPart::String("Hello, ".to_string()), - MacroStringPart::MacroUsage("world".to_string()), - MacroStringPart::String("! $(world".to_string()), - ]) + MacroString::MacroString { + parts: vec![ + MacroStringPart::String("Hello, ".to_string()), + MacroStringPart::MacroUsage("world".to_string()), + MacroStringPart::String("! $(world".to_string()), + ], + prepare_variables.is_empty(): true, + } ); - assert_eq!( + assert_struct!( MacroString::from_str("Hello $(a) from $(b) and $(c)").unwrap(), - MacroString::MacroString(vec![ - MacroStringPart::String("Hello ".to_string()), - MacroStringPart::MacroUsage("a".to_string()), - MacroStringPart::String(" from ".to_string()), - MacroStringPart::MacroUsage("b".to_string()), - MacroStringPart::String(" and ".to_string()), - MacroStringPart::MacroUsage("c".to_string()), - ]) + MacroString::MacroString { + parts: vec![ + MacroStringPart::String("Hello ".to_string()), + MacroStringPart::MacroUsage("a".to_string()), + MacroStringPart::String(" from ".to_string()), + MacroStringPart::MacroUsage("b".to_string()), + MacroStringPart::String(" and ".to_string()), + MacroStringPart::MacroUsage("c".to_string()), + ], + prepare_variables.is_empty(): true, + } ); - assert_eq!( + assert_struct!( MacroString::from_str("Hello, $(world! $(world)!").unwrap(), - MacroString::MacroString(vec![ - MacroStringPart::String("Hello, $(world! ".to_string()), - MacroStringPart::MacroUsage("world".to_string()), - MacroStringPart::String("!".to_string()), - ]) + MacroString::MacroString { + parts: vec![ + MacroStringPart::String("Hello, $(world! ".to_string()), + MacroStringPart::MacroUsage("world".to_string()), + MacroStringPart::String("!".to_string()), + ], + prepare_variables.is_empty(): true, + } ); } } diff --git a/src/transpile/variables.rs b/src/transpile/variables.rs index dc7011c..8708933 100644 --- a/src/transpile/variables.rs +++ b/src/transpile/variables.rs @@ -32,12 +32,12 @@ use super::{ AssignmentError, IllegalAnnotationContent, IllegalIndexing, IllegalIndexingReason, MismatchedTypes, NotComptime, }, - expression::{ComptimeValue, DataLocation, ExpectedType, StorageType}, + expression::{DataLocation, ExpectedType, StorageType}, FunctionData, TranspileAnnotationValue, TranspileError, TranspileResult, }; #[cfg(feature = "shulkerbox")] -use super::{internal_functions::InternalFunction, Transpiler}; +use super::{expression::ComptimeValue, internal_functions::InternalFunction, Transpiler}; /// Stores the data required to access a variable. #[cfg(feature = "shulkerbox")] @@ -409,6 +409,8 @@ impl Transpiler { handler, )?; if is_global { + use shulkerbox::datapack::Group; + let (temp_objective, [temp_target]) = self.get_temp_scoreboard_locations_array(); let test_cmd = match declaration.variable_type().keyword { KeywordKind::Int => { @@ -425,7 +427,7 @@ impl Transpiler { Condition::Atom( format!("score {temp_target} {temp_objective} matches 0").into(), ), - Box::new(Execute::Run(Box::new(Command::Group(cmds)))), + Box::new(Execute::Run(Box::new(Command::Group(Group::new(cmds))))), None, )); Ok(vec![test_exists_cmd, cond_cmd]) @@ -634,9 +636,11 @@ impl Transpiler { } else { let index_span = match destination { TranspileAssignmentTarget::Indexed(_, expr) => expr.span(), - TranspileAssignmentTarget::Identifier(_) => unreachable!( - "indexing value must be present (checked before)" - ), + TranspileAssignmentTarget::Identifier(_) => { + unreachable!( + "indexing value must be present (checked before)" + ) + } }; let err = TranspileError::IllegalIndexing(IllegalIndexing { expression: index_span, @@ -688,9 +692,11 @@ impl Transpiler { } else { let index_span = match destination { TranspileAssignmentTarget::Indexed(_, expr) => expr.span(), - TranspileAssignmentTarget::Identifier(_) => unreachable!( - "indexing value must be present (checked before)" - ), + TranspileAssignmentTarget::Identifier(_) => { + unreachable!( + "indexing value must be present (checked before)" + ) + } }; let err = TranspileError::IllegalIndexing(IllegalIndexing { expression: index_span, @@ -847,18 +853,20 @@ impl Transpiler { // TODO: change invalid criteria if boolean if !crate::util::is_valid_scoreboard_objective_name(&name_eval) { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation must be a valid scoreboard objective name.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation must be a valid scoreboard objective name." + .to_string(), + }); handler.receive(Box::new(err.clone())); return Err(err); } Ok(name_eval) } else { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation could not have been evaluated at compile time.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation could not have been evaluated at compile time." + .to_string(), + }); handler.receive(Box::new(err.clone())); Err(err) } @@ -950,34 +958,36 @@ impl Transpiler { // TODO: change invalid criteria if boolean if !crate::util::is_valid_scoreboard_objective_name(&name_eval) { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' must be a valid scoreboard objective name.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'name' must be a valid scoreboard objective name.".to_string(), + }); handler.receive(Box::new(err.clone())); return Err(err); } if !crate::util::is_valid_scoreboard_target(&target_eval) { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'target' must be a valid scoreboard player name.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'target' must be a valid scoreboard player name.".to_string(), + }); handler.receive(Box::new(err.clone())); return Err(err); } Ok((name_eval, target_eval)) } else { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' or 'target' could not have been evaluated at compile time.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: "Deobfuscate annotation 'name' or 'target' could not have been evaluated at compile time.".to_string(), + }); handler.receive(Box::new(err.clone())); Err(err) } } else { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: deobfuscate_annotation.span(), - message: "Deobfuscate annotation 'name' and 'target' must be compile time expressions.".to_string() - }); + annotation: deobfuscate_annotation.span(), + message: + "Deobfuscate annotation 'name' and 'target' must be compile time expressions." + .to_string(), + }); handler.receive(Box::new(err.clone())); Err(err) } @@ -1180,9 +1190,7 @@ impl Transpiler { { Ok(Vec::new()) } else if matches!(target_type, StorageType::Boolean) { - let cmd = Command::Raw(format!( - "data modify storage {target_storage_name} {target_path} set from storage {storage_name} {path}" - )); + let cmd = Command::Raw(format!("data modify storage {target_storage_name} {target_path} set from storage {storage_name} {path}")); Ok(vec![cmd]) } else { let err = TranspileError::MismatchedTypes(MismatchedTypes { @@ -1194,6 +1202,37 @@ impl Transpiler { } } }, + StorageType::String => match to { + DataLocation::ScoreboardValue { .. } | DataLocation::Tag { .. } => { + let err = TranspileError::MismatchedTypes(MismatchedTypes { + expression: expression.span(), + expected_type: to.value_type().into(), + }); + handler.receive(Box::new(err.clone())); + Err(err) + } + DataLocation::Storage { + storage_name: to_storage_name, + path: to_path, + r#type, + } => { + if r#type == &StorageType::String { + if storage_name == to_storage_name && path == to_path { + Ok(Vec::new()) + } else { + let cmd = Command::Raw(format!("data modify storage {to_storage_name} {to_path} set from storage {storage_name} {path}")); + Ok(vec![cmd]) + } + } else { + let err = TranspileError::MismatchedTypes(MismatchedTypes { + expression: expression.span(), + expected_type: to.value_type().into(), + }); + handler.receive(Box::new(err.clone())); + Err(err) + } + } + }, }, DataLocation::ScoreboardValue { objective, @@ -1206,9 +1245,7 @@ impl Transpiler { if objective == target_objective && score_target == target_target { Ok(Vec::new()) } else { - let cmd = Command::Raw(format!( - "scoreboard players operation {target_target} {target_objective} = {score_target} {objective}" - )); + let cmd = Command::Raw(format!("scoreboard players operation {target_target} {target_objective} = {score_target} {objective}")); Ok(vec![cmd]) } }