implement integer and boolean function arguments

This commit is contained in:
Moritz Hölting 2025-03-30 19:38:08 +02:00
parent ca0edfc5bc
commit f3b3d5d3b6
9 changed files with 758 additions and 444 deletions

View File

@ -52,6 +52,7 @@ pub enum KeywordKind {
Replace, Replace,
Int, Int,
Bool, Bool,
Macro,
} }
impl Display for KeywordKind { impl Display for KeywordKind {
@ -117,6 +118,7 @@ impl KeywordKind {
Self::Replace => "replace", Self::Replace => "replace",
Self::Int => "int", Self::Int => "int",
Self::Bool => "bool", Self::Bool => "bool",
Self::Macro => "macro",
} }
} }

View File

@ -165,7 +165,7 @@ impl Function {
parameters parameters
.elements() .elements()
.map(|el| el.span.str().to_string()) .map(|el| el.identifier().span.str().to_string())
.collect() .collect()
} else { } else {
HashSet::new() HashSet::new()

View File

@ -4,6 +4,7 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use enum_as_inner::EnumAsInner;
use getset::Getters; use getset::Getters;
use crate::{ use crate::{
@ -87,7 +88,7 @@ impl Declaration {
/// ; /// ;
/// ///
/// ParameterList: /// ParameterList:
/// Identifier (',' Identifier)* ','? /// FunctionArgument (',' FunctionArgument)* ','?
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -104,7 +105,7 @@ pub struct Function {
#[get = "pub"] #[get = "pub"]
open_paren: Punctuation, open_paren: Punctuation,
#[get = "pub"] #[get = "pub"]
parameters: Option<ConnectedList<Identifier, Punctuation>>, parameters: Option<ConnectedList<FunctionParameter, Punctuation>>,
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
#[get = "pub"] #[get = "pub"]
@ -123,7 +124,7 @@ impl Function {
Keyword, Keyword,
Identifier, Identifier,
Punctuation, Punctuation,
Option<ConnectedList<Identifier, Punctuation>>, Option<ConnectedList<FunctionParameter, Punctuation>>,
Punctuation, Punctuation,
Block, Block,
) { ) {
@ -156,6 +157,41 @@ impl SourceElement for Function {
} }
} }
// Represents a variable type keyword for function arguments.
///
/// Syntax Synopsis:
///
/// ``` ebnf
/// FunctionVariableType:
/// 'macro' | 'int' | 'bool'
/// ;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumAsInner)]
pub enum FunctionVariableType {
Macro(Keyword),
Integer(Keyword),
Boolean(Keyword),
}
/// Represents a function argument in the syntax tree.
///
/// Syntax Synopsis:
///
/// ``` ebnf
/// FunctionArgument:
/// FunctionVariableType Identifier
/// ;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)]
pub struct FunctionParameter {
#[get = "pub"]
variable_type: FunctionVariableType,
#[get = "pub"]
identifier: Identifier,
}
/// Represents an import declaration in the syntax tree. /// Represents an import declaration in the syntax tree.
/// ///
/// Syntax Synopsis: /// Syntax Synopsis:
@ -451,7 +487,7 @@ impl Parser<'_> {
let delimited_tree = self.parse_enclosed_list( let delimited_tree = self.parse_enclosed_list(
Delimiter::Parenthesis, Delimiter::Parenthesis,
',', ',',
|parser: &mut Parser<'_>| parser.parse_identifier(handler), |parser: &mut Parser<'_>| parser.parse_function_parameter(handler),
handler, handler,
)?; )?;
@ -479,4 +515,57 @@ impl Parser<'_> {
} }
} }
} }
fn parse_function_parameter(
&mut self,
handler: &impl Handler<base::Error>,
) -> ParseResult<FunctionParameter> {
match self.stop_at_significant() {
Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Int => {
let variable_type = FunctionVariableType::Integer(keyword);
self.forward();
let identifier = self.parse_identifier(handler)?;
Ok(FunctionParameter {
variable_type,
identifier,
})
}
Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Bool => {
let variable_type = FunctionVariableType::Boolean(keyword);
self.forward();
let identifier = self.parse_identifier(handler)?;
Ok(FunctionParameter {
variable_type,
identifier,
})
}
Reading::Atomic(Token::Keyword(keyword)) if keyword.keyword == KeywordKind::Macro => {
let variable_type = FunctionVariableType::Macro(keyword);
self.forward();
let identifier = self.parse_identifier(handler)?;
Ok(FunctionParameter {
variable_type,
identifier,
})
}
unexpected => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::Either(&[
SyntaxKind::Keyword(KeywordKind::Int),
SyntaxKind::Keyword(KeywordKind::Bool),
SyntaxKind::Keyword(KeywordKind::Macro),
]),
found: unexpected.into_token(),
});
handler.receive(err.clone());
Err(err)
}
}
}
} }

565
src/transpile/function.rs Normal file
View File

@ -0,0 +1,565 @@
use chksum_md5 as md5;
use enum_as_inner::EnumAsInner;
use std::{collections::BTreeMap, sync::Arc};
use shulkerbox::datapack::{Command, Execute};
use crate::{
base::{
self,
source_file::{SourceElement, Span},
Handler,
},
semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments},
syntax::syntax_tree::{
declaration::FunctionVariableType,
expression::{Expression, Primary},
statement::Statement,
},
transpile::{
error::{IllegalAnnotationContent, MissingFunctionDeclaration},
util::{MacroString, MacroStringPart},
},
};
use super::{
error::{MismatchedTypes, TranspileError, TranspileResult, UnknownIdentifier},
expression::{ComptimeValue, ExpectedType, StorageType},
variables::{Scope, VariableData},
FunctionData, TranspileAnnotationValue, Transpiler,
};
#[derive(Debug, Clone)]
pub enum TranspiledFunctionArguments {
None,
Static(BTreeMap<String, MacroString>),
Dynamic(Vec<Command>),
}
impl Transpiler {
/// Gets the function at the given path, or transpiles it if it hasn't been transpiled yet.
/// Returns the location of the function or None if the function does not exist.
#[tracing::instrument(level = "trace", skip(self, handler))]
pub(super) fn get_or_transpile_function(
&mut self,
identifier_span: &Span,
arguments: Option<&[&Expression]>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<(String, TranspiledFunctionArguments)> {
let program_identifier = identifier_span.source_file().identifier();
let program_query = (
program_identifier.to_string(),
identifier_span.str().to_string(),
);
let alias_query = self.aliases.get(&program_query).cloned();
let already_transpiled = scope
.get_variable(identifier_span.str())
.expect("called function should be in scope")
.as_ref()
.as_function()
.map(|(_, path)| path.get().is_some())
.expect("called variable should be of type function");
let function_data = scope
.get_variable(identifier_span.str())
.or_else(|| {
alias_query
.clone()
.and_then(|(alias_program_identifier, alias_function_name)| {
self.scopes
.get(&alias_program_identifier)
.and_then(|s| s.get_variable(&alias_function_name))
})
})
.ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope),
);
handler.receive(error.clone());
error
})?;
let VariableData::Function {
function_data,
path: function_path,
} = function_data.as_ref()
else {
unreachable!("must be of correct type, otherwise errored out before");
};
if !already_transpiled {
tracing::trace!("Function not transpiled yet, transpiling.");
let statements = function_data.statements.clone();
let modified_name = function_data.annotations.get("deobfuscate").map_or_else(
|| {
let hash_data = program_identifier.to_string() + "\0" + identifier_span.str();
Ok("shu/".to_string() + &md5::hash(hash_data).to_hex_lowercase())
},
|val| match val {
TranspileAnnotationValue::None => Ok(identifier_span.str().to_string()),
TranspileAnnotationValue::Expression(expr) => expr
.comptime_eval(scope, handler)
.and_then(|val| val.to_string_no_macro())
.ok_or_else(|| {
let err = TranspileError::IllegalAnnotationContent(
IllegalAnnotationContent {
annotation: identifier_span.clone(),
message: "Cannot evaluate annotation at compile time"
.to_string(),
},
);
handler.receive(err.clone());
err
}),
TranspileAnnotationValue::Map(_) => {
let err =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: identifier_span.clone(),
message: "Deobfuscate annotation cannot be a map.".to_string(),
});
handler.receive(err.clone());
Err(err)
}
},
)?;
let function_location = format!(
"{namespace}:{modified_name}",
namespace = function_data.namespace
);
function_path.set(function_location.clone()).unwrap();
let function_scope = Scope::with_parent(scope);
for (i, param) in function_data.parameters.iter().enumerate() {
let param_str = param.identifier().span.str();
match param.variable_type() {
FunctionVariableType::Macro(_) => {
function_scope.set_variable(
param_str,
VariableData::MacroParameter {
index: i,
macro_name: crate::util::identifier_to_macro(param_str).to_string(),
},
);
}
FunctionVariableType::Integer(_) => {
let objective = format!(
"shu_arguments_{}",
function_location.replace(['/', ':'], "_")
);
function_scope.set_variable(
param_str,
VariableData::ScoreboardValue {
objective: objective.clone(),
target: crate::util::identifier_to_scoreboard_target(param_str)
.into_owned(),
},
);
}
FunctionVariableType::Boolean(_) => {
let storage_name = format!(
"shulkerscript:arguments_{}",
function_location.replace(['/', ':'], "_")
);
// TODO: replace with proper path
function_scope.set_variable(
param_str,
VariableData::BooleanStorage {
storage_name,
path: crate::util::identifier_to_scoreboard_target(param_str)
.into_owned(),
},
);
}
}
}
let commands =
self.transpile_function(&statements, program_identifier, &function_scope, handler)?;
let namespace = self.datapack.namespace_mut(&function_data.namespace);
if namespace.function(&modified_name).is_some() {
let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames {
name: modified_name,
definition: identifier_span.clone(),
});
handler.receive(err.clone());
return Err(err);
}
let function = namespace.function_mut(&modified_name);
function.get_commands_mut().extend(commands);
if function_data.annotations.contains_key("tick") {
self.datapack.add_tick(&function_location);
}
if function_data.annotations.contains_key("load") {
self.datapack.add_load(&function_location);
}
}
let function_location = function_path
.get()
.ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope),
);
handler.receive(error.clone());
error
})
.map(String::to_owned)?;
let args = self.transpile_function_arguments(
function_data,
&function_location,
arguments,
scope,
handler,
)?;
Ok((function_location, args))
}
fn transpile_function(
&mut self,
statements: &[Statement],
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let mut errors = Vec::new();
let commands = statements
.iter()
.flat_map(|statement| {
self.transpile_statement(statement, program_identifier, scope, handler)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect();
if !errors.is_empty() {
return Err(errors.remove(0));
}
Ok(commands)
}
#[expect(clippy::too_many_lines)]
fn transpile_function_arguments(
&mut self,
function_data: &FunctionData,
function_location: &str,
arguments: Option<&[&Expression]>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<TranspiledFunctionArguments> {
let parameters = &function_data.parameters;
let identifier_span = &function_data.identifier_span;
let arg_count = arguments.map(<[&Expression]>::len);
match arg_count {
Some(arg_count) if arg_count != parameters.len() => {
let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments {
expected: parameters.len(),
actual: arg_count,
span: identifier_span.clone(),
});
handler.receive(err.clone());
Err(err)
}
Some(arg_count) if arg_count > 0 => {
#[derive(Debug, Clone, EnumAsInner)]
enum Parameter {
Static(MacroString),
Storage {
prepare_cmds: Vec<Command>,
storage_name: String,
path: String,
},
}
let mut compiled_args = Vec::<Parameter>::new();
let mut errs = Vec::new();
for expression in arguments.iter().flat_map(|expressions| expressions.iter()) {
let value = match expression {
Expression::Primary(Primary::Lua(lua)) => {
lua.eval_comptime(scope, handler).and_then(|val| match val {
Some(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)),
Some(val) => Ok(Parameter::Static(val.to_macro_string())),
None => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: expression.span(),
expected_type: ExpectedType::String,
});
handler.receive(err.clone());
Err(err)
}
})
}
Expression::Primary(Primary::Integer(num)) => {
Ok(Parameter::Static(num.span.str().to_string().into()))
}
Expression::Primary(Primary::Boolean(bool)) => {
Ok(Parameter::Static(bool.span.str().to_string().into()))
}
Expression::Primary(Primary::StringLiteral(string)) => {
Ok(Parameter::Static(string.str_content().to_string().into()))
}
Expression::Primary(Primary::MacroStringLiteral(literal)) => {
Ok(Parameter::Static(literal.into()))
}
Expression::Primary(primary @ Primary::Identifier(ident)) => {
let var = scope.get_variable(ident.span.str()).ok_or_else(|| {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: ident.span(),
});
handler.receive(err.clone());
err
})?;
match var.as_ref() {
VariableData::MacroParameter { macro_name, .. } => {
Ok(Parameter::Static(MacroString::MacroString(vec![
MacroStringPart::MacroUsage(macro_name.clone()),
])))
}
VariableData::BooleanStorage { .. }
| VariableData::ScoreboardValue { .. } => {
let (temp_storage, mut temp_path) =
self.get_temp_storage_locations(1);
let prepare_cmds = self.transpile_primary_expression(
primary,
&super::expression::DataLocation::Storage {
storage_name: temp_storage.clone(),
path: temp_path[0].clone(),
r#type: match var.as_ref() {
VariableData::BooleanStorage { .. } => {
StorageType::Boolean
}
VariableData::ScoreboardValue { .. } => {
StorageType::Int
}
_ => unreachable!("checked in parent match"),
},
},
scope,
handler,
)?;
Ok(Parameter::Storage {
prepare_cmds,
storage_name: temp_storage,
path: std::mem::take(&mut temp_path[0]),
})
}
_ => todo!("other variable types"),
}
}
Expression::Primary(
Primary::Parenthesized(_)
| Primary::Prefix(_)
| Primary::Indexed(_)
| Primary::FunctionCall(_),
)
| Expression::Binary(_) => {
let (temp_storage, mut temp_path) = self.get_temp_storage_locations(1);
let prepare_cmds = self.transpile_expression(
expression,
&super::expression::DataLocation::Storage {
storage_name: temp_storage.clone(),
path: temp_path[0].clone(),
r#type: StorageType::Int,
},
scope,
handler,
)?;
Ok(Parameter::Storage {
prepare_cmds,
storage_name: temp_storage,
path: std::mem::take(&mut temp_path[0]),
})
}
};
match value {
Ok(value) => {
compiled_args.push(value);
}
Err(err) => {
compiled_args
.push(Parameter::Static(MacroString::String(String::new())));
errs.push(err.clone());
}
}
}
if let Some(err) = errs.first() {
return Err(err.clone());
}
if compiled_args.iter().any(|arg| !arg.is_static()) {
let (mut setup_cmds, move_cmds, static_params) = parameters.clone().into_iter().zip(compiled_args).fold(
(Vec::new(), Vec::new(), BTreeMap::new()),
|(mut acc_setup, mut acc_move, mut statics), (param, data)| {
match param.variable_type() {
FunctionVariableType::Macro(_) => {
let arg_name = crate::util::identifier_to_macro(param.identifier().span.str());
match data {
Parameter::Static(s) => {
match s {
MacroString::String(value) => statics.insert(
arg_name.to_string(),
MacroString::String(crate::util::escape_str(&value).to_string())
),
MacroString::MacroString(parts) => {
let parts = parts.into_iter().map(|part| {
match part {
MacroStringPart::String(s) => MacroStringPart::String(crate::util::escape_str(&s).to_string()),
MacroStringPart::MacroUsage(m) => MacroStringPart::MacroUsage(m),
}
}).collect();
statics.insert(arg_name.to_string(), MacroString::MacroString(parts))
}
};
}
Parameter::Storage { prepare_cmds, storage_name, path } => {
acc_setup.extend(prepare_cmds);
acc_move.push(Command::Raw(
format!(r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}")
));
}
}
}
FunctionVariableType::Integer(_) => {
let objective = format!("shu_arguments_{}", function_location.replace(['/', ':'], "_"));
let param_str = param.identifier().span.str();
let target = crate::util::identifier_to_scoreboard_target(param_str);
match data {
Parameter::Static(s) => {
match s.as_str() {
Ok(s) => {
if s.parse::<i32>().is_ok() {
acc_move.push(Command::Raw(format!(r"scoreboard players set {target} {objective} {s}")));
} else {
panic!("non-integer static argument")
}
}
Err(parts) => {
acc_move.push(Command::UsesMacro(MacroString::MacroString(
std::iter::once(MacroStringPart::String(format!("scoreboard players set {target} {objective} ")))
.chain(parts.iter().cloned()).collect()
).into()));
}
}
}
Parameter::Storage { prepare_cmds, storage_name, path } => {
acc_setup.extend(prepare_cmds);
acc_move.push(Command::Execute(Execute::Store(
format!("result score {target} {objective}").into(),
Box::new(Execute::Run(Box::new(Command::Raw(format!("data get storage {storage_name} {path}")))))
)));
}
}
},
FunctionVariableType::Boolean(_) => {
let target_storage_name = format!("shulkerscript:arguments_{}", function_location.replace(['/', ':'], "_"));
let param_str = param.identifier().span.str();
let target_path = crate::util::identifier_to_scoreboard_target(param_str);
match data {
Parameter::Static(s) => {
match s.as_str() {
Ok(s) => {
if let Ok(b) = s.parse::<bool>() {
acc_move.push(Command::Raw(format!("data modify storage {target_storage_name} {target_path} set value {}", if b { "1b" } else { "0b" })));
} else {
panic!("non-integer static argument")
}
}
Err(parts) => {
acc_move.push(Command::UsesMacro(MacroString::MacroString(
std::iter::once(MacroStringPart::String(format!("data modify storage {target_storage_name} {target_path} set value ")))
.chain(parts.iter().cloned()).collect()
).into()));
}
}
}
Parameter::Storage { prepare_cmds, storage_name, path } => {
acc_setup.extend(prepare_cmds);
acc_move.push(Command::Raw(format!("data modify storage {target_storage_name} {target_path} set from storage {storage_name} {path}")));
}
}
},
}
(acc_setup, acc_move, statics)},
);
let statics_len = static_params.len();
let joined_statics =
super::util::join_macro_strings(static_params.into_iter().enumerate().map(
|(i, (k, v))| match v {
MacroString::String(s) => {
let mut s = format!(r#"{k}:"{s}""#);
if i < statics_len - 1 {
s.push(',');
}
MacroString::String(s)
}
MacroString::MacroString(mut parts) => {
parts.insert(0, MacroStringPart::String(format!(r#"{k}:""#)));
let mut ending = '"'.to_string();
if i < statics_len - 1 {
ending.push(',');
}
parts.push(MacroStringPart::String(ending));
MacroString::MacroString(parts)
}
},
));
let statics_cmd = match joined_statics {
MacroString::String(s) => Command::Raw(format!(
r"data merge storage shulkerscript:function_arguments {{{s}}}"
)),
MacroString::MacroString(_) => Command::UsesMacro(
super::util::join_macro_strings([
MacroString::String(
"data merge storage shulkerscript:function_arguments {"
.to_string(),
),
joined_statics,
MacroString::String("}".to_string()),
])
.into(),
),
};
setup_cmds.push(statics_cmd);
setup_cmds.extend(move_cmds);
Ok(TranspiledFunctionArguments::Dynamic(setup_cmds))
} else {
let function_args = parameters
.clone()
.into_iter()
.zip(
compiled_args
.into_iter()
.map(|arg| arg.into_static().expect("checked in if condition")),
)
.map(|(k, v)| (k.identifier().span.str().to_string(), v))
.collect();
Ok(TranspiledFunctionArguments::Static(function_args))
}
}
_ => Ok(TranspiledFunctionArguments::None),
}
}
}

View File

@ -311,7 +311,7 @@ fn print_function(
todo!("throw error when index is not constant integer") todo!("throw error when index is not constant integer")
} }
} }
_ => todo!(), _ => todo!("catch illegal indexing"),
} }
} }
_ => Err(TranspileError::IllegalIndexing(IllegalIndexing { _ => Err(TranspileError::IllegalIndexing(IllegalIndexing {

View File

@ -7,7 +7,10 @@ use std::{
use crate::{ use crate::{
base::source_file::{SourceElement, Span}, base::source_file::{SourceElement, Span},
syntax::syntax_tree::{expression::Expression, statement::Statement, AnnotationValue}, syntax::syntax_tree::{
declaration::FunctionParameter, expression::Expression, statement::Statement,
AnnotationValue,
},
}; };
#[doc(hidden)] #[doc(hidden)]
@ -32,6 +35,11 @@ pub use transpiler::Transpiler;
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
pub mod internal_functions; pub mod internal_functions;
#[doc(hidden)]
#[cfg(feature = "shulkerbox")]
pub mod function;
pub use function::TranspiledFunctionArguments;
mod variables; mod variables;
pub use variables::{Scope, VariableData}; pub use variables::{Scope, VariableData};
@ -42,7 +50,7 @@ pub mod util;
pub struct FunctionData { pub struct FunctionData {
pub(super) namespace: String, pub(super) namespace: String,
pub(super) identifier_span: Span, pub(super) identifier_span: Span,
pub(super) parameters: Vec<String>, pub(super) parameters: Vec<FunctionParameter>,
pub(super) statements: Vec<Statement>, pub(super) statements: Vec<Statement>,
pub(super) public: bool, pub(super) public: bool,
pub(super) annotations: HashMap<String, TranspileAnnotationValue>, pub(super) annotations: HashMap<String, TranspileAnnotationValue>,

View File

@ -1,7 +1,5 @@
//! Transpiler for `Shulkerscript` //! Transpiler for `Shulkerscript`
use chksum_md5 as md5;
use enum_as_inner::EnumAsInner;
use std::{ use std::{
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
ops::Deref, ops::Deref,
@ -11,12 +9,8 @@ use std::{
use shulkerbox::datapack::{self, Command, Datapack, Execute}; use shulkerbox::datapack::{self, Command, Datapack, Execute};
use crate::{ use crate::{
base::{ base::{self, source_file::SourceElement, Handler},
self, semantic::error::UnexpectedExpression,
source_file::{SourceElement, Span},
Handler,
},
semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression},
syntax::syntax_tree::{ syntax::syntax_tree::{
declaration::{Declaration, ImportItems}, declaration::{Declaration, ImportItems},
expression::{Expression, FunctionCall, Primary}, expression::{Expression, FunctionCall, Primary},
@ -27,17 +21,14 @@ use crate::{
}, },
AnnotationAssignment, AnnotationAssignment,
}, },
transpile::{ transpile::util::{MacroString, MacroStringPart},
error::{IllegalAnnotationContent, MissingFunctionDeclaration},
util::{MacroString, MacroStringPart},
},
}; };
use super::{ use super::{
error::{MismatchedTypes, TranspileError, TranspileResult, UnknownIdentifier}, error::{MismatchedTypes, TranspileError, TranspileResult},
expression::{ComptimeValue, ExpectedType, ExtendedCondition, StorageType}, expression::{ComptimeValue, ExpectedType, ExtendedCondition},
variables::{Scope, TranspileAssignmentTarget, VariableData}, variables::{Scope, TranspileAssignmentTarget, VariableData},
FunctionData, TranspileAnnotationValue, FunctionData, TranspileAnnotationValue, TranspiledFunctionArguments,
}; };
/// A transpiler for `Shulkerscript`. /// A transpiler for `Shulkerscript`.
@ -49,18 +40,11 @@ pub struct Transpiler {
pub(super) initialized_constant_scores: HashSet<i64>, pub(super) initialized_constant_scores: HashSet<i64>,
pub(super) temp_counter: usize, pub(super) temp_counter: usize,
/// Top-level [`Scope`] for each program identifier /// Top-level [`Scope`] for each program identifier
scopes: BTreeMap<String, Arc<Scope<'static>>>, pub(super) scopes: BTreeMap<String, Arc<Scope<'static>>>,
/// Key: (program identifier, function name) /// Key: (program identifier, function name)
functions: BTreeMap<(String, String), FunctionData>, pub(super) functions: BTreeMap<(String, String), FunctionData>,
/// Key: alias, Value: target /// Key: alias, Value: target
aliases: HashMap<(String, String), (String, String)>, pub(super) aliases: HashMap<(String, String), (String, String)>,
}
#[derive(Debug, Clone)]
pub enum TranspiledFunctionArguments {
None,
Static(BTreeMap<String, MacroString>),
Dynamic(Vec<Command>),
} }
impl Transpiler { impl Transpiler {
@ -202,11 +186,7 @@ impl Transpiler {
parameters: function parameters: function
.parameters() .parameters()
.as_ref() .as_ref()
.map(|l| { .map(|l| l.elements().cloned().collect::<Vec<_>>())
l.elements()
.map(|i| i.span.str().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default(), .unwrap_or_default(),
statements, statements,
public: function.is_public(), public: function.is_public(),
@ -266,412 +246,7 @@ impl Transpiler {
}; };
} }
/// Gets the function at the given path, or transpiles it if it hasn't been transpiled yet. pub(super) fn transpile_statement(
/// Returns the location of the function or None if the function does not exist.
#[tracing::instrument(level = "trace", skip(self, handler))]
pub(super) fn get_or_transpile_function(
&mut self,
identifier_span: &Span,
arguments: Option<&[&Expression]>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<(String, TranspiledFunctionArguments)> {
let program_identifier = identifier_span.source_file().identifier();
let program_query = (
program_identifier.to_string(),
identifier_span.str().to_string(),
);
let alias_query = self.aliases.get(&program_query).cloned();
let already_transpiled = scope
.get_variable(identifier_span.str())
.expect("called function should be in scope")
.as_ref()
.as_function()
.map(|(_, path)| path.get().is_some())
.expect("called variable should be of type function");
let function_data = scope
.get_variable(identifier_span.str())
.or_else(|| {
alias_query
.clone()
.and_then(|(alias_program_identifier, alias_function_name)| {
self.scopes
.get(&alias_program_identifier)
.and_then(|s| s.get_variable(&alias_function_name))
})
})
.ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope),
);
handler.receive(error.clone());
error
})?;
let VariableData::Function {
function_data,
path: function_path,
} = function_data.as_ref()
else {
unreachable!("must be of correct type, otherwise errored out before");
};
if !already_transpiled {
tracing::trace!("Function not transpiled yet, transpiling.");
let function_scope = Scope::with_parent(scope);
for (i, param) in function_data.parameters.iter().enumerate() {
function_scope.set_variable(
param,
VariableData::MacroParameter {
index: i,
macro_name: crate::util::identifier_to_macro(param).to_string(),
},
);
}
let statements = function_data.statements.clone();
let modified_name = function_data.annotations.get("deobfuscate").map_or_else(
|| {
let hash_data = program_identifier.to_string() + "\0" + identifier_span.str();
Ok("shu/".to_string() + &md5::hash(hash_data).to_hex_lowercase())
},
|val| match val {
TranspileAnnotationValue::None => Ok(identifier_span.str().to_string()),
TranspileAnnotationValue::Expression(expr) => expr
.comptime_eval(scope, handler)
.and_then(|val| val.to_string_no_macro())
.ok_or_else(|| {
let err = TranspileError::IllegalAnnotationContent(
IllegalAnnotationContent {
annotation: identifier_span.clone(),
message: "Cannot evaluate annotation at compile time"
.to_string(),
},
);
handler.receive(err.clone());
err
}),
TranspileAnnotationValue::Map(_) => {
let err =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: identifier_span.clone(),
message: "Deobfuscate annotation cannot be a map.".to_string(),
});
handler.receive(err.clone());
Err(err)
}
},
)?;
let function_location = format!(
"{namespace}:{modified_name}",
namespace = function_data.namespace
);
function_path.set(function_location.clone()).unwrap();
let commands =
self.transpile_function(&statements, program_identifier, &function_scope, handler)?;
let namespace = self.datapack.namespace_mut(&function_data.namespace);
if namespace.function(&modified_name).is_some() {
let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames {
name: modified_name,
definition: identifier_span.clone(),
});
handler.receive(err.clone());
return Err(err);
}
let function = namespace.function_mut(&modified_name);
function.get_commands_mut().extend(commands);
if function_data.annotations.contains_key("tick") {
self.datapack.add_tick(&function_location);
}
if function_data.annotations.contains_key("load") {
self.datapack.add_load(&function_location);
}
}
let parameters = &function_data.parameters;
let function_location = function_path
.get()
.ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope),
);
handler.receive(error.clone());
error
})
.map(String::to_owned)?;
let arg_count = arguments.map(<[&Expression]>::len);
if arg_count.is_some_and(|arg_count| arg_count != parameters.len()) {
let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments {
expected: parameters.len(),
actual: arg_count.unwrap_or_default(),
span: identifier_span.clone(),
});
handler.receive(err.clone());
Err(err)
} else if arg_count.is_some_and(|arg_count| arg_count > 0) {
{
#[derive(Debug, Clone, EnumAsInner)]
enum Parameter {
Static(MacroString),
Dynamic {
prepare_cmds: Vec<Command>,
storage_name: String,
path: String,
},
}
let mut compiled_args = Vec::new();
let mut errs = Vec::new();
for expression in arguments.iter().flat_map(|x| x.iter()) {
let value = match expression {
Expression::Primary(Primary::Lua(lua)) => {
lua.eval_comptime(scope, handler).and_then(|val| match val {
Some(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)),
Some(val) => Ok(Parameter::Static(val.to_macro_string())),
None => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: expression.span(),
expected_type: ExpectedType::String,
});
handler.receive(err.clone());
Err(err)
}
})
}
Expression::Primary(Primary::Integer(num)) => {
Ok(Parameter::Static(num.span.str().to_string().into()))
}
Expression::Primary(Primary::Boolean(bool)) => {
Ok(Parameter::Static(bool.span.str().to_string().into()))
}
Expression::Primary(Primary::StringLiteral(string)) => {
Ok(Parameter::Static(string.str_content().to_string().into()))
}
Expression::Primary(Primary::MacroStringLiteral(literal)) => {
Ok(Parameter::Static(literal.into()))
}
Expression::Primary(primary @ Primary::Identifier(ident)) => {
let var = scope.get_variable(ident.span.str()).ok_or_else(|| {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: ident.span(),
});
handler.receive(err.clone());
err
})?;
match var.as_ref() {
VariableData::MacroParameter { macro_name, .. } => {
Ok(Parameter::Static(MacroString::MacroString(vec![
MacroStringPart::MacroUsage(macro_name.clone()),
])))
}
VariableData::BooleanStorage { .. }
| VariableData::ScoreboardValue { .. } => {
let (temp_storage, mut temp_path) =
self.get_temp_storage_locations(1);
let prepare_cmds = self.transpile_primary_expression(
primary,
&super::expression::DataLocation::Storage {
storage_name: temp_storage.clone(),
path: temp_path[0].clone(),
r#type: match var.as_ref() {
VariableData::BooleanStorage { .. } => {
StorageType::Boolean
}
VariableData::ScoreboardValue { .. } => {
StorageType::Int
}
_ => unreachable!("checked in parent match"),
},
},
scope,
handler,
)?;
Ok(Parameter::Dynamic {
prepare_cmds,
storage_name: temp_storage,
path: std::mem::take(&mut temp_path[0]),
})
}
_ => todo!("other variable types"),
}
}
Expression::Primary(
Primary::Parenthesized(_)
| Primary::Prefix(_)
| Primary::Indexed(_)
| Primary::FunctionCall(_),
)
| Expression::Binary(_) => {
let (temp_storage, mut temp_path) = self.get_temp_storage_locations(1);
let prepare_cmds = self.transpile_expression(
expression,
&super::expression::DataLocation::Storage {
storage_name: temp_storage.clone(),
path: temp_path[0].clone(),
r#type: StorageType::Int,
},
scope,
handler,
)?;
Ok(Parameter::Dynamic {
prepare_cmds,
storage_name: temp_storage,
path: std::mem::take(&mut temp_path[0]),
})
}
};
match value {
Ok(value) => {
compiled_args.push(value);
}
Err(err) => {
compiled_args
.push(Parameter::Static(MacroString::String(String::new())));
errs.push(err.clone());
}
}
}
if let Some(err) = errs.first() {
return Err(err.clone());
}
if compiled_args.iter().any(|arg| !arg.is_static()) {
let (mut setup_cmds, move_cmds, static_params) = parameters.clone().into_iter().zip(compiled_args).fold(
(Vec::new(), Vec::new(), BTreeMap::new()),
|(mut acc_setup, mut acc_move, mut statics), (arg_name, data)| {
let arg_name = crate::util::identifier_to_macro(&arg_name);
match data {
Parameter::Static(s) => {
match s {
MacroString::String(value) => statics.insert(arg_name.to_string(), MacroString::String(crate::util::escape_str(&value).to_string())),
MacroString::MacroString(parts) => {
let parts = parts.into_iter().map(|part| {
match part {
MacroStringPart::String(s) => MacroStringPart::String(crate::util::escape_str(&s).to_string()),
MacroStringPart::MacroUsage(m) => MacroStringPart::MacroUsage(m),
}
}).collect();
statics.insert(arg_name.to_string(), MacroString::MacroString(parts))
}
};
}
Parameter::Dynamic { prepare_cmds, storage_name, path } => {
acc_setup.extend(prepare_cmds);
acc_move.push(Command::Raw(format!(r"data modify storage shulkerscript:function_arguments {arg_name} set from storage {storage_name} {path}")));
}
}
(acc_setup, acc_move, statics)},
);
let statics_len = static_params.len();
let joined_statics =
super::util::join_macro_strings(static_params.into_iter().enumerate().map(
|(i, (k, v))| match v {
MacroString::String(s) => {
let mut s = format!(r#"{k}:"{s}""#);
if i < statics_len - 1 {
s.push(',');
}
MacroString::String(s)
}
MacroString::MacroString(mut parts) => {
parts.insert(0, MacroStringPart::String(format!(r#"{k}:""#)));
let mut ending = '"'.to_string();
if i < statics_len - 1 {
ending.push(',');
}
parts.push(MacroStringPart::String(ending));
MacroString::MacroString(parts)
}
},
));
let statics_cmd = match joined_statics {
MacroString::String(s) => Command::Raw(format!(
r"data merge storage shulkerscript:function_arguments {{{s}}}"
)),
MacroString::MacroString(_) => Command::UsesMacro(
super::util::join_macro_strings([
MacroString::String(
"data merge storage shulkerscript:function_arguments {"
.to_string(),
),
joined_statics,
MacroString::String("}".to_string()),
])
.into(),
),
};
setup_cmds.push(statics_cmd);
setup_cmds.extend(move_cmds);
Ok((
function_location,
TranspiledFunctionArguments::Dynamic(setup_cmds),
))
} else {
let function_args = parameters
.clone()
.into_iter()
.zip(
compiled_args
.into_iter()
.map(|arg| arg.into_static().expect("checked in if condition")),
)
.collect();
Ok((
function_location,
TranspiledFunctionArguments::Static(function_args),
))
}
}
} else {
Ok((function_location, TranspiledFunctionArguments::None))
}
}
fn transpile_function(
&mut self,
statements: &[Statement],
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let mut errors = Vec::new();
let commands = statements
.iter()
.flat_map(|statement| {
self.transpile_statement(statement, program_identifier, scope, handler)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect();
if !errors.is_empty() {
return Err(errors.remove(0));
}
Ok(commands)
}
fn transpile_statement(
&mut self, &mut self,
statement: &Statement, statement: &Statement,
program_identifier: &str, program_identifier: &str,

View File

@ -44,6 +44,39 @@ impl Display for MacroString {
} }
} }
impl MacroString {
/// Check if the macro string contains any macros
#[must_use]
pub fn contains_macros(&self) -> bool {
match self {
Self::String(_) => false,
Self::MacroString(parts) => parts
.iter()
.any(|p| matches!(p, MacroStringPart::MacroUsage(_))),
}
}
/// Get the string representation of the macro string or the parts if it contains macros
///
/// # Errors
/// - If the macro string contains macros
pub fn as_str(&self) -> Result<std::borrow::Cow<str>, &[MacroStringPart]> {
match self {
Self::String(s) => Ok(std::borrow::Cow::Borrowed(s)),
Self::MacroString(parts) if self.contains_macros() => Err(parts),
Self::MacroString(parts) => Ok(std::borrow::Cow::Owned(
parts
.iter()
.map(|p| match p {
MacroStringPart::String(s) => s.clone(),
MacroStringPart::MacroUsage(m) => format!("$({m})"),
})
.collect::<String>(),
)),
}
}
}
fn normalize_program_identifier<S>(identifier: S) -> String fn normalize_program_identifier<S>(identifier: S) -> String
where where
S: AsRef<str>, S: AsRef<str>,

View File

@ -79,6 +79,48 @@ pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow<str> {
} }
} }
/// Transforms an identifier to a macro name that only contains `a-zA-Z0-9_`.
#[cfg(feature = "shulkerbox")]
#[must_use]
pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow<str> {
if !(..=16).contains(&ident.len())
|| ident
.chars()
.any(|c| c != '_' || !c.is_ascii_alphanumeric())
{
std::borrow::Cow::Owned(chksum_md5::hash(ident).to_hex_lowercase().split_off(16))
} else {
std::borrow::Cow::Borrowed(ident)
}
}
/// Transforms an identifier to a name that only contains `a-zA-Z0-9_`.
/// Does only strip invalid characters if the `shulkerbox` feature is not enabled.
#[cfg(not(feature = "shulkerbox"))]
#[must_use]
pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow<str> {
if !(..=16).contains(&ident.len())
|| ident
.chars()
.any(|c| c != '_' || !c.is_ascii_alphanumeric())
{
let new_ident = ident
.chars()
.map(|c| {
if *c != '_' && !c.is_ascii_alphanumeric() {
'_'
} else {
c
}
})
.collect::<String>();
std::borrow::Cow::Owned(new_ident)
} else {
std::borrow::Cow::Borrowed(ident)
}
}
/// Returns whether a string is a valid scoreboard name. /// Returns whether a string is a valid scoreboard name.
#[must_use] #[must_use]
pub fn is_valid_scoreboard_objective_name(name: &str) -> bool { pub fn is_valid_scoreboard_objective_name(name: &str) -> bool {