shulkerscript-lang/src/transpile/function.rs

575 lines
28 KiB
Rust

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)
.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(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 {
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(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]),
})
}
_ => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: expression.span(),
expected_type: ExpectedType::AnyOf(vec![
ExpectedType::Integer,
ExpectedType::Boolean,
ExpectedType::String,
]),
});
handler.receive(err.clone());
Err(err)
}
}
}
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),
}
}
}