diff --git a/CHANGELOG.md b/CHANGELOG.md index 2775cdd..75c2b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Member access (e.g. `.objective` to get objective name where int is stored) - Return statement - internal `print` function +- reserve `while` and `for` keyword - Example: barebones compiler ### Changed diff --git a/grammar.md b/grammar.md index aa2432a..9011717 100644 --- a/grammar.md +++ b/grammar.md @@ -219,7 +219,7 @@ Semicolon: ```ebnf FunctionVariableType: - 'macro' | 'int' | 'bool' + 'macro' | 'int' | 'bool' | 'val' ; ``` diff --git a/src/lexical/token.rs b/src/lexical/token.rs index ec0f231..8920574 100644 --- a/src/lexical/token.rs +++ b/src/lexical/token.rs @@ -54,6 +54,8 @@ pub enum KeywordKind { Macro, Val, Return, + While, + For, } impl Display for KeywordKind { @@ -121,6 +123,8 @@ impl KeywordKind { Self::Macro => "macro", Self::Val => "val", Self::Return => "return", + Self::While => "while", + Self::For => "for", } } diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index 7b72328..cb99cff 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -161,7 +161,12 @@ impl Function { ) -> Result<(), error::Error> { let child_scope = SemanticScope::with_parent(scope); - if let Some(parameters) = self.parameters().as_ref().map(ConnectedList::elements) { + if let Some(parameters) = self + .parameters() + .as_ref() + .map(ConnectedList::elements) + .map(Iterator::collect::>) + { if let Some(incompatible) = self.annotations().iter().find(|a| { ["tick", "load", "uninstall"].contains(&a.assignment().identifier.span.str()) }) { @@ -174,6 +179,25 @@ impl Function { }); handler.receive(err.clone()); return Err(err); + } else if parameters + .iter() + .any(|param| matches!(param.variable_type(), FunctionVariableType::Value(_))) + { + if let Some(incompatible) = self + .annotations() + .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(), + }); + handler.receive(err.clone()); + return Err(err); + } } for param in parameters { @@ -182,6 +206,7 @@ impl Function { FunctionVariableType::Boolean(_) => VariableType::BooleanStorage, FunctionVariableType::Integer(_) => VariableType::ScoreboardValue, FunctionVariableType::Macro(_) => VariableType::MacroParameter, + FunctionVariableType::Value(_) => VariableType::ComptimeValue, }; child_scope.set_variable(name, var); } diff --git a/src/syntax/syntax_tree/declaration.rs b/src/syntax/syntax_tree/declaration.rs index dcef071..dcd814b 100644 --- a/src/syntax/syntax_tree/declaration.rs +++ b/src/syntax/syntax_tree/declaration.rs @@ -179,7 +179,7 @@ impl SourceElement for Function { /// /// ```ebnf /// FunctionVariableType: -/// 'macro' | 'int' | 'bool' +/// 'macro' | 'int' | 'bool' | 'val' /// ; /// ``` #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -188,6 +188,7 @@ pub enum FunctionVariableType { Macro(Keyword), Integer(Keyword), Boolean(Keyword), + Value(Keyword), } /// Represents a function argument in the syntax tree. @@ -607,12 +608,24 @@ impl Parser<'_> { identifier, }) } + Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Val => { + let variable_type = FunctionVariableType::Value(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), + SyntaxKind::Keyword(KeywordKind::Val), ]), found: unexpected.into_token(), }); diff --git a/src/transpile/expression.rs b/src/transpile/expression.rs index 6398a46..60d0812 100644 --- a/src/transpile/expression.rs +++ b/src/transpile/expression.rs @@ -17,16 +17,17 @@ use super::{ }, Scope, TranspileResult, Transpiler, VariableData, }; -use crate::syntax::syntax_tree::expression::{Indexed, Parenthesized}; #[cfg(feature = "shulkerbox")] use crate::{ base::{self, source_file::SourceElement, Handler, VoidHandler}, lexical::token::{Identifier, MacroStringLiteralPart, StringLiteral}, syntax::syntax_tree::expression::{ - Binary, BinaryOperator, Expression, MemberAccess, PrefixOperator, Primary, + Binary, BinaryOperator, Expression, Indexed, MemberAccess, Parenthesized, PrefixOperator, + Primary, }, transpile::{ error::{FunctionArgumentsNotAllowed, MissingValue}, + variables::FunctionVariableDataType, TranspileError, }, }; @@ -579,7 +580,10 @@ impl Identifier { }), } } - VariableData::Function { path, .. } => { + VariableData::Function { + variable_data: FunctionVariableDataType::Simple { path, .. }, + .. + } => { match member_access.member().span.str() { "path" => { #[expect(clippy::option_if_let_else)] @@ -597,7 +601,11 @@ impl Identifier { }), } } - VariableData::InternalFunction { .. } => Err(NotComptime { + VariableData::Function { + variable_data: FunctionVariableDataType::ComptimeArguments { .. }, + .. + } + | VariableData::InternalFunction { .. } => Err(NotComptime { expression: member_access.member().span(), }), VariableData::MacroParameter { macro_name, .. } => { diff --git a/src/transpile/function.rs b/src/transpile/function.rs index c695c0f..72dc07f 100644 --- a/src/transpile/function.rs +++ b/src/transpile/function.rs @@ -1,6 +1,11 @@ use chksum_md5 as md5; use enum_as_inner::EnumAsInner; -use std::{borrow::ToOwned, collections::BTreeMap, sync::Arc}; +use itertools::Itertools; +use std::{ + borrow::{Cow, ToOwned}, + collections::BTreeMap, + sync::{Arc, RwLock}, +}; use shulkerbox::datapack::{Command, Execute}; @@ -19,6 +24,7 @@ use crate::{ transpile::{ error::{IllegalAnnotationContent, MissingFunctionDeclaration}, util::{MacroString, MacroStringPart}, + variables::FunctionVariableDataType, }, }; @@ -39,7 +45,6 @@ pub enum TranspiledFunctionArguments { 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. - #[expect(clippy::too_many_lines)] #[tracing::instrument(level = "trace", skip(self, handler))] pub(super) fn get_or_transpile_function( &mut self, @@ -50,13 +55,6 @@ impl Transpiler { ) -> TranspileResult<(String, TranspiledFunctionArguments)> { let program_identifier = identifier_span.source_file().identifier(); let function = scope.get_variable(identifier_span.str()); - let already_transpiled = function - .as_ref() - .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 = function.ok_or_else(|| { let err = TranspileError::MissingFunctionDeclaration( @@ -66,150 +64,246 @@ impl Transpiler { err })?; - let VariableData::Function { - function_data, - path: function_path, - function_scope, - } = function_data.as_ref() - else { - unreachable!("must be of correct type, otherwise errored out before"); - }; + let (function_data, function_scope, function_type) = function_data + .as_ref() + .as_function() + .expect("called variable should be of type function"); - if !already_transpiled { - tracing::trace!("Function not transpiled yet, transpiling."); + match function_type { + FunctionVariableDataType::Simple { + path: function_path, + } => { + let already_transpiled = function_path.get().is_some(); - let statements = function_data.statements.clone(); + if !already_transpiled { + self.prepare_transpile_function( + function_data, + program_identifier, + identifier_span, + |path| function_path.set(path).expect("not set before"), + std::iter::repeat(None), + None, + function_scope, + scope, + handler, + )?; + } - 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) - .ok() - .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(Box::new(err.clone())); - err - }), - TranspileAnnotationValue::Map(_, span) => { + let function_location = function_path + .get() + .ok_or_else(|| { + let err = TranspileError::MissingFunctionDeclaration( + MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), + ); + handler.receive(Box::new(err.clone())); + err + }) + .map(String::to_owned)?; + + let args = self.transpile_function_arguments( + function_data, + &function_location, + arguments, + scope, + handler, + )?; + + Ok((function_location, args)) + } + FunctionVariableDataType::ComptimeArguments { function_paths } => { + let comptime_args = self.transpile_comptime_function_arguments( + function_data, + arguments, + scope, + handler, + )?; + let hash = comptime_args_hash(&comptime_args); + + let read_guard = function_paths.read().unwrap(); + let function_location = if let Some(data) = read_guard.get(&hash) { + data.to_owned() + } else { + drop(read_guard); + let function_scope = Scope::with_parent(function_scope.to_owned()); + let mut path = String::new(); + + self.prepare_transpile_function( + function_data, + program_identifier, + identifier_span, + |p| path = p, + comptime_args.into_iter(), + Some(&hash), + &function_scope, + scope, + handler, + )?; + + function_paths + .write() + .unwrap() + .insert(hash.clone(), path.clone()); + + path + }; + + let args = self.transpile_function_arguments( + function_data, + &function_location, + arguments, + scope, + handler, + )?; + + Ok((function_location, args)) + } + } + } + + #[expect(clippy::too_many_arguments, clippy::too_many_lines)] + fn prepare_transpile_function( + &mut self, + function_data: &FunctionData, + program_identifier: &str, + identifier_span: &Span, + set_function_path: impl FnOnce(String), + comptime_args: impl Iterator>, + function_suffix: Option<&str>, + function_scope: &Arc, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult<()> { + tracing::trace!("Function not transpiled yet, transpiling."); + + let statements = function_data.statements.clone(); + + let mut 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) + .ok() + .and_then(|val| val.to_string_no_macro()) + .ok_or_else(|| { let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { - annotation: span.clone(), - message: "Deobfuscate annotation cannot be a map.".to_string(), + annotation: identifier_span.clone(), + message: "Cannot evaluate annotation at compile time".to_string(), }); handler.receive(Box::new(err.clone())); - Err(err) - } - }, - )?; - - let function_location = format!( - "{namespace}:{modified_name}", - namespace = function_data.namespace - ); - - function_path.set(function_location.clone()).unwrap(); - - 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(), - }, - ); - } + err + }), + TranspileAnnotationValue::Map(_, span) => { + let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { + annotation: span.clone(), + message: "Deobfuscate annotation cannot be a map.".to_string(), + }); + handler.receive(Box::new(err.clone())); + Err(err) } - } + }, + )?; - let commands = - self.transpile_function(&statements, program_identifier, function_scope, handler)?; + if let Some(suffix) = function_suffix { + use std::fmt::Write as _; - let namespace = self.datapack.namespace_mut(&function_data.namespace); + let _ = write!(modified_name, "/{suffix}"); + } - if namespace.function(&modified_name).is_some() { - let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames { - name: modified_name, - definition: identifier_span.clone(), - }); - handler.receive(Box::new(err.clone())); - return Err(err); - } + let function_location = format!( + "{namespace}:{modified_name}", + namespace = function_data.namespace + ); - let function = namespace.function_mut(&modified_name); - function.get_commands_mut().extend(commands); + set_function_path(function_location.clone()); - 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); + for (i, (param, comptime_arg)) in function_data + .parameters + .iter() + .zip(comptime_args) + .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(), + }, + ); + } + FunctionVariableType::Value(_) => { + function_scope.set_variable( + param_str, + VariableData::ComptimeValue { + value: Arc::new(RwLock::new(comptime_arg)), + read_only: false, + }, + ); + } } } - let function_location = function_path - .get() - .ok_or_else(|| { - let err = TranspileError::MissingFunctionDeclaration( - MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope), - ); - handler.receive(Box::new(err.clone())); - err - }) - .map(String::to_owned)?; + let commands = + self.transpile_function(&statements, program_identifier, function_scope, handler)?; - let args = self.transpile_function_arguments( - function_data, - &function_location, - arguments, - scope, - handler, - )?; + let namespace = self.datapack.namespace_mut(&function_data.namespace); - Ok((function_location, args)) + if namespace.function(&modified_name).is_some() { + let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames { + name: modified_name, + definition: identifier_span.clone(), + }); + handler.receive(Box::new(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); + } + + Ok(()) } fn transpile_function( @@ -238,6 +332,51 @@ impl Transpiler { Ok(commands) } + #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)] + fn transpile_comptime_function_arguments( + &mut self, + function_data: &FunctionData, + arguments: Option<&[&Expression]>, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult>> { + let parameters = &function_data.parameters; + let arg_count = arguments.map(<[&Expression]>::len).unwrap_or_default(); + + let mut has_comptime = parameters + .iter() + .any(|param| matches!(param.variable_type(), FunctionVariableType::Value(_))); + + if has_comptime { + if parameters.len() == arg_count { + let vals = parameters + .iter() + .zip(arguments.iter().flat_map(|args| args.iter())) + .map(|(param, arg)| match param.variable_type() { + FunctionVariableType::Value(_) => { + has_comptime = true; + let val = arg.comptime_eval(scope, handler)?; + Ok(Some(val)) + } + _ => Ok(None), + }) + .collect::>>>()?; + + Ok(vals) + } else { + let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments { + expected: parameters.len(), + actual: arg_count, + span: function_data.identifier_span.clone(), + }); + handler.receive(Box::new(err.clone())); + Err(err) + } + } else { + Ok(vec![None; parameters.len()]) + } + } + #[expect(clippy::too_many_lines)] fn transpile_function_arguments( &mut self, @@ -264,6 +403,7 @@ impl Transpiler { Some(arg_count) if arg_count > 0 => { #[derive(Debug, Clone, EnumAsInner)] enum Parameter { + Comptime, Static(MacroString), Storage { prepare_cmds: Vec, @@ -275,64 +415,119 @@ impl Transpiler { 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 { - Ok(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)), - Ok(val) => Ok(Parameter::Static(val.to_macro_string())), - Err(err) => { - let err = TranspileError::NotComptime(err); - handler.receive(Box::new(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(Box::new(err.clone())); - err - })?; - match var.as_ref() { - VariableData::MacroParameter { macro_name, .. } => { - Ok(Parameter::Static(MacroString::MacroString(vec![ - MacroStringPart::MacroUsage(macro_name.clone()), - ]))) - } + for (expression, is_comptime) in arguments + .iter() + .flat_map(|expressions| expressions.iter()) + .zip( + parameters + .iter() + .map(|p| matches!(p.variable_type(), FunctionVariableType::Value(_))), + ) + { + let value = if is_comptime { + Ok(Parameter::Comptime) + } else { + match expression { + Expression::Primary(Primary::Lua(lua)) => { + lua.eval_comptime(scope, handler).and_then(|val| match val { + Ok(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)), + Ok(val) => Ok(Parameter::Static(val.to_macro_string())), + Err(err) => { + let err = TranspileError::NotComptime(err); + handler.receive(Box::new(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(Box::new(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, [temp_path]) = - self.get_temp_storage_locations_array(); - let prepare_cmds = self.transpile_primary_expression( - primary, - &super::expression::DataLocation::Storage { - storage_name: temp_storage.clone(), - path: temp_path.clone(), - r#type: match var.as_ref() { - VariableData::BooleanStorage { .. } => { - StorageType::Boolean - } - VariableData::ScoreboardValue { .. } => { - StorageType::Int - } - _ => unreachable!("checked in parent match"), + VariableData::BooleanStorage { .. } + | VariableData::ScoreboardValue { .. } => { + let (temp_storage, [temp_path]) = + self.get_temp_storage_locations_array(); + let prepare_cmds = self.transpile_primary_expression( + primary, + &super::expression::DataLocation::Storage { + storage_name: temp_storage.clone(), + path: temp_path.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: temp_path, + }) + } + _ => { + let err = + TranspileError::MismatchedTypes(MismatchedTypes { + expression: expression.span(), + expected_type: ExpectedType::AnyOf(vec![ + ExpectedType::Integer, + ExpectedType::Boolean, + ExpectedType::String, + ]), + }); + handler.receive(Box::new(err.clone())); + Err(err) + } + } + } + Expression::Primary(Primary::MemberAccess(member_access)) => { + if let Ok(value) = member_access.parent().comptime_member_access( + member_access, + scope, + handler, + ) { + Ok(Parameter::Static(value.to_macro_string())) + } else { + let (storage_name, [path]) = + self.get_temp_storage_locations_array(); + let prepare_cmds = self.transpile_expression( + expression, + &super::expression::DataLocation::Storage { + storage_name: storage_name.clone(), + path: path.clone(), + r#type: StorageType::Int, }, scope, handler, @@ -340,39 +535,25 @@ impl Transpiler { Ok(Parameter::Storage { prepare_cmds, - storage_name: temp_storage, - path: temp_path, + storage_name, + path, }) } - _ => { - let err = TranspileError::MismatchedTypes(MismatchedTypes { - expression: expression.span(), - expected_type: ExpectedType::AnyOf(vec![ - ExpectedType::Integer, - ExpectedType::Boolean, - ExpectedType::String, - ]), - }); - handler.receive(Box::new(err.clone())); - Err(err) - } } - } - Expression::Primary(Primary::MemberAccess(member_access)) => { - if let Ok(value) = member_access.parent().comptime_member_access( - member_access, - scope, - handler, - ) { - Ok(Parameter::Static(value.to_macro_string())) - } else { - let (storage_name, [path]) = + Expression::Primary( + Primary::Parenthesized(_) + | Primary::Prefix(_) + | Primary::Indexed(_) + | Primary::FunctionCall(_), + ) + | Expression::Binary(_) => { + let (temp_storage, [temp_path]) = self.get_temp_storage_locations_array(); let prepare_cmds = self.transpile_expression( expression, &super::expression::DataLocation::Storage { - storage_name: storage_name.clone(), - path: path.clone(), + storage_name: temp_storage.clone(), + path: temp_path.clone(), r#type: StorageType::Int, }, scope, @@ -381,37 +562,11 @@ impl Transpiler { Ok(Parameter::Storage { prepare_cmds, - storage_name, - path, + storage_name: temp_storage, + path: temp_path, }) } } - Expression::Primary( - Primary::Parenthesized(_) - | Primary::Prefix(_) - | Primary::Indexed(_) - | Primary::FunctionCall(_), - ) - | Expression::Binary(_) => { - let (temp_storage, [temp_path]) = - self.get_temp_storage_locations_array(); - let prepare_cmds = self.transpile_expression( - expression, - &super::expression::DataLocation::Storage { - storage_name: temp_storage.clone(), - path: temp_path.clone(), - r#type: StorageType::Int, - }, - scope, - handler, - )?; - - Ok(Parameter::Storage { - prepare_cmds, - storage_name: temp_storage, - path: temp_path, - }) - } }; match value { @@ -437,6 +592,7 @@ impl Transpiler { FunctionVariableType::Macro(_) => { let arg_name = crate::util::identifier_to_macro(param.identifier().span.str()); match data { + Parameter::Comptime => {} Parameter::Static(s) => { match s { MacroString::String(value) => statics.insert( @@ -469,6 +625,7 @@ impl Transpiler { let target = crate::util::identifier_to_scoreboard_target(param_str); match data { + Parameter::Comptime => {} Parameter::Static(s) => { match s.as_str() { Ok(s) => { @@ -502,6 +659,7 @@ impl Transpiler { let target_path = crate::util::identifier_to_scoreboard_target(param_str); match data { + Parameter::Comptime => {} Parameter::Static(s) => { match s.as_str() { Ok(s) => { @@ -525,6 +683,9 @@ impl Transpiler { } } }, + FunctionVariableType::Value(_) => { + // handled before in `transpile_comptime_function_arguments` + } } (require_dyn_params, acc_setup, acc_move, statics)}, ); @@ -608,3 +769,31 @@ impl Transpiler { } } } + +fn comptime_args_hash(args: &[Option]) -> String { + let combined = args + .iter() + .filter_map(Option::as_ref) + .map(|arg| match arg { + ComptimeValue::Boolean(b) => Cow::Owned(b.to_string()), + ComptimeValue::Integer(i) => Cow::Owned(i.to_string()), + ComptimeValue::String(s) => Cow::Borrowed(s.as_str()), + ComptimeValue::MacroString(s) => match s.as_str() { + Ok(s) => s, + Err(parts) => { + let s = parts + .iter() + .map(|p| match p { + MacroStringPart::String(s) => Cow::Borrowed(s.as_str()), + MacroStringPart::MacroUsage(u) => Cow::Owned(format!("`{u}`")), + }) + .join("\0\0"); + + Cow::Owned(s) + } + }, + }) + .join("\0"); + + md5::hash(combined).to_hex_lowercase() +} diff --git a/src/transpile/lua.rs b/src/transpile/lua.rs index 4761713..6109244 100644 --- a/src/transpile/lua.rs +++ b/src/transpile/lua.rs @@ -426,7 +426,7 @@ mod disabled { handler: &impl Handler, ) -> TranspileResult<((), ())> { let _ = scope; - handler.receive(TranspileError::LuaDisabled); + handler.receive(Box::new(TranspileError::LuaDisabled)); tracing::error!("Lua code evaluation is disabled"); Err(TranspileError::LuaDisabled) } @@ -440,9 +440,9 @@ mod disabled { &self, scope: &Arc, handler: &impl Handler, - ) -> TranspileResult> { + ) -> TranspileResult> { let _ = scope; - handler.receive(TranspileError::LuaDisabled); + handler.receive(Box::new(TranspileError::LuaDisabled)); tracing::error!("Lua code evaluation is disabled"); Err(TranspileError::LuaDisabled) } diff --git a/src/transpile/mod.rs b/src/transpile/mod.rs index 7779460..5e0265b 100644 --- a/src/transpile/mod.rs +++ b/src/transpile/mod.rs @@ -16,6 +16,7 @@ use crate::{ #[doc(hidden)] #[cfg(feature = "shulkerbox")] pub mod conversions; + pub mod error; pub mod expression; diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index 7b9e3dd..5dfc9a3 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -1,9 +1,9 @@ //! Transpiler for `Shulkerscript` use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, ops::Deref, - sync::{Arc, OnceLock}, + sync::{Arc, OnceLock, RwLock}, }; use itertools::Itertools; @@ -13,7 +13,7 @@ use crate::{ base::{self, source_file::SourceElement, Handler}, semantic::error::UnexpectedExpression, syntax::syntax_tree::{ - declaration::{Declaration, ImportItems}, + declaration::{Declaration, FunctionVariableType, ImportItems}, expression::{Expression, FunctionCall, PrefixOperator, Primary}, program::{Namespace, ProgramFile}, statement::{ @@ -25,6 +25,7 @@ use crate::{ transpile::{ error::IllegalAnnotationContent, util::{MacroString, MacroStringPart}, + variables::FunctionVariableDataType, }, }; @@ -240,6 +241,7 @@ impl Transpiler { } /// Transpiles the given declaration. + #[expect(clippy::too_many_lines)] fn transpile_declaration( &mut self, declaration: &Declaration, @@ -283,12 +285,25 @@ impl Transpiler { public: function.is_public(), annotations, }; + let has_comptime_params = function_data + .parameters + .iter() + .any(|param| matches!(param.variable_type(), FunctionVariableType::Value(_))); + let function_scope = Scope::with_parent(scope.clone()); scope.set_variable( &name, VariableData::Function { function_data, - path: OnceLock::new(), - function_scope: Scope::with_parent(scope.clone()), + function_scope, + variable_data: if has_comptime_params { + FunctionVariableDataType::ComptimeArguments { + function_paths: RwLock::new(HashMap::new()), + } + } else { + FunctionVariableDataType::Simple { + path: OnceLock::new(), + } + }, }, ); } @@ -297,8 +312,6 @@ impl Transpiler { let import_identifier = super::util::calculate_import_identifier(&program_identifier, path); - // let aliases = &mut self.aliases; - match import.items() { ImportItems::All(_) => { handler.receive(base::Error::Other( diff --git a/src/transpile/variables.rs b/src/transpile/variables.rs index 1ca5c08..dc7011c 100644 --- a/src/transpile/variables.rs +++ b/src/transpile/variables.rs @@ -47,10 +47,10 @@ pub enum VariableData { Function { /// The function data. function_data: FunctionData, - /// The path to the function once it is generated. - path: OnceLock, /// The scope of the function. function_scope: Arc, + /// The variable data. + variable_data: FunctionVariableDataType, }, /// A macro function parameter. MacroParameter { @@ -111,6 +111,34 @@ pub enum VariableData { }, } +#[cfg(feature = "shulkerbox")] +#[derive(Debug)] +pub enum FunctionVariableDataType { + Simple { + /// The path to the function once it is generated. + path: OnceLock, + }, + ComptimeArguments { + /// The paths of the functions with different comptime arguments by hash. + function_paths: RwLock>, + }, +} + +#[cfg(feature = "shulkerbox")] +impl Clone for FunctionVariableDataType { + fn clone(&self) -> Self { + match self { + Self::Simple { path } => Self::Simple { path: path.clone() }, + Self::ComptimeArguments { function_paths } => { + let function_paths = function_paths.read().unwrap(); + Self::ComptimeArguments { + function_paths: RwLock::new(function_paths.clone()), + } + } + } + } +} + #[derive(Debug, Clone, Copy, EnumAsInner)] pub enum TranspileAssignmentTarget<'a> { Identifier(&'a Identifier), diff --git a/src/util.rs b/src/util.rs index 7d5396d..fedaf46 100644 --- a/src/util.rs +++ b/src/util.rs @@ -62,7 +62,7 @@ pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow<'_, str> { /// Does only strip invalid characters if the `shulkerbox` feature is not enabled. #[cfg(not(feature = "shulkerbox"))] #[must_use] -pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow { +pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow<'_, str> { if ident.contains("__") || ident .chars() @@ -98,7 +98,7 @@ pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow<'_, str> /// 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 { +pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow<'_, str> { if !(..=16).contains(&ident.len()) || ident .chars()