implement compile-time function arguments

This commit is contained in:
Moritz Hölting 2025-08-19 15:14:12 +02:00
parent bd8270bd5d
commit 3bc3ca180f
12 changed files with 548 additions and 266 deletions

View File

@ -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

View File

@ -219,7 +219,7 @@ Semicolon:
```ebnf
FunctionVariableType:
'macro' | 'int' | 'bool'
'macro' | 'int' | 'bool' | 'val'
;
```

View File

@ -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",
}
}

View File

@ -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::<Vec<_>>)
{
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);
}

View File

@ -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(),
});

View File

@ -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, .. } => {

View File

@ -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<Item = Option<ComptimeValue>>,
function_suffix: Option<&str>,
function_scope: &Arc<Scope>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> 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<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Option<ComptimeValue>>> {
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::<TranspileResult<Vec<Option<ComptimeValue>>>>()?;
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<Command>,
@ -275,64 +415,119 @@ impl Transpiler {
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(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<ComptimeValue>]) -> 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()
}

View File

@ -426,7 +426,7 @@ mod disabled {
handler: &impl Handler<base::Error>,
) -> 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<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Option<ComptimeValue>> {
) -> TranspileResult<Result<ComptimeValue, crate::transpile::error::NotComptime>> {
let _ = scope;
handler.receive(TranspileError::LuaDisabled);
handler.receive(Box::new(TranspileError::LuaDisabled));
tracing::error!("Lua code evaluation is disabled");
Err(TranspileError::LuaDisabled)
}

View File

@ -16,6 +16,7 @@ use crate::{
#[doc(hidden)]
#[cfg(feature = "shulkerbox")]
pub mod conversions;
pub mod error;
pub mod expression;

View File

@ -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(

View File

@ -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<String>,
/// The scope of the function.
function_scope: Arc<Scope>,
/// 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<String>,
},
ComptimeArguments {
/// The paths of the functions with different comptime arguments by hash.
function_paths: RwLock<HashMap<String, String>>,
},
}
#[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),

View File

@ -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<str> {
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<str> {
pub fn identifier_to_scoreboard_target(ident: &str) -> std::borrow::Cow<'_, str> {
if !(..=16).contains(&ident.len())
|| ident
.chars()