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) - Member access (e.g. `.objective` to get objective name where int is stored)
- Return statement - Return statement
- internal `print` function - internal `print` function
- reserve `while` and `for` keyword
- Example: barebones compiler - Example: barebones compiler
### Changed ### Changed

View File

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

View File

@ -54,6 +54,8 @@ pub enum KeywordKind {
Macro, Macro,
Val, Val,
Return, Return,
While,
For,
} }
impl Display for KeywordKind { impl Display for KeywordKind {
@ -121,6 +123,8 @@ impl KeywordKind {
Self::Macro => "macro", Self::Macro => "macro",
Self::Val => "val", Self::Val => "val",
Self::Return => "return", Self::Return => "return",
Self::While => "while",
Self::For => "for",
} }
} }

View File

@ -161,7 +161,12 @@ impl Function {
) -> Result<(), error::Error> { ) -> Result<(), error::Error> {
let child_scope = SemanticScope::with_parent(scope); 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| { if let Some(incompatible) = self.annotations().iter().find(|a| {
["tick", "load", "uninstall"].contains(&a.assignment().identifier.span.str()) ["tick", "load", "uninstall"].contains(&a.assignment().identifier.span.str())
}) { }) {
@ -174,6 +179,25 @@ impl Function {
}); });
handler.receive(err.clone()); handler.receive(err.clone());
return Err(err); 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 { for param in parameters {
@ -182,6 +206,7 @@ impl Function {
FunctionVariableType::Boolean(_) => VariableType::BooleanStorage, FunctionVariableType::Boolean(_) => VariableType::BooleanStorage,
FunctionVariableType::Integer(_) => VariableType::ScoreboardValue, FunctionVariableType::Integer(_) => VariableType::ScoreboardValue,
FunctionVariableType::Macro(_) => VariableType::MacroParameter, FunctionVariableType::Macro(_) => VariableType::MacroParameter,
FunctionVariableType::Value(_) => VariableType::ComptimeValue,
}; };
child_scope.set_variable(name, var); child_scope.set_variable(name, var);
} }

View File

@ -179,7 +179,7 @@ impl SourceElement for Function {
/// ///
/// ```ebnf /// ```ebnf
/// FunctionVariableType: /// FunctionVariableType:
/// 'macro' | 'int' | 'bool' /// 'macro' | 'int' | 'bool' | 'val'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -188,6 +188,7 @@ pub enum FunctionVariableType {
Macro(Keyword), Macro(Keyword),
Integer(Keyword), Integer(Keyword),
Boolean(Keyword), Boolean(Keyword),
Value(Keyword),
} }
/// Represents a function argument in the syntax tree. /// Represents a function argument in the syntax tree.
@ -607,12 +608,24 @@ impl Parser<'_> {
identifier, 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 => { unexpected => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax { let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::Either(&[ expected: SyntaxKind::Either(&[
SyntaxKind::Keyword(KeywordKind::Int), SyntaxKind::Keyword(KeywordKind::Int),
SyntaxKind::Keyword(KeywordKind::Bool), SyntaxKind::Keyword(KeywordKind::Bool),
SyntaxKind::Keyword(KeywordKind::Macro), SyntaxKind::Keyword(KeywordKind::Macro),
SyntaxKind::Keyword(KeywordKind::Val),
]), ]),
found: unexpected.into_token(), found: unexpected.into_token(),
}); });

View File

@ -17,16 +17,17 @@ use super::{
}, },
Scope, TranspileResult, Transpiler, VariableData, Scope, TranspileResult, Transpiler, VariableData,
}; };
use crate::syntax::syntax_tree::expression::{Indexed, Parenthesized};
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
use crate::{ use crate::{
base::{self, source_file::SourceElement, Handler, VoidHandler}, base::{self, source_file::SourceElement, Handler, VoidHandler},
lexical::token::{Identifier, MacroStringLiteralPart, StringLiteral}, lexical::token::{Identifier, MacroStringLiteralPart, StringLiteral},
syntax::syntax_tree::expression::{ syntax::syntax_tree::expression::{
Binary, BinaryOperator, Expression, MemberAccess, PrefixOperator, Primary, Binary, BinaryOperator, Expression, Indexed, MemberAccess, Parenthesized, PrefixOperator,
Primary,
}, },
transpile::{ transpile::{
error::{FunctionArgumentsNotAllowed, MissingValue}, error::{FunctionArgumentsNotAllowed, MissingValue},
variables::FunctionVariableDataType,
TranspileError, TranspileError,
}, },
}; };
@ -579,7 +580,10 @@ impl Identifier {
}), }),
} }
} }
VariableData::Function { path, .. } => { VariableData::Function {
variable_data: FunctionVariableDataType::Simple { path, .. },
..
} => {
match member_access.member().span.str() { match member_access.member().span.str() {
"path" => { "path" => {
#[expect(clippy::option_if_let_else)] #[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(), expression: member_access.member().span(),
}), }),
VariableData::MacroParameter { macro_name, .. } => { VariableData::MacroParameter { macro_name, .. } => {

View File

@ -1,6 +1,11 @@
use chksum_md5 as md5; use chksum_md5 as md5;
use enum_as_inner::EnumAsInner; 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}; use shulkerbox::datapack::{Command, Execute};
@ -19,6 +24,7 @@ use crate::{
transpile::{ transpile::{
error::{IllegalAnnotationContent, MissingFunctionDeclaration}, error::{IllegalAnnotationContent, MissingFunctionDeclaration},
util::{MacroString, MacroStringPart}, util::{MacroString, MacroStringPart},
variables::FunctionVariableDataType,
}, },
}; };
@ -39,7 +45,6 @@ pub enum TranspiledFunctionArguments {
impl Transpiler { impl Transpiler {
/// Gets the function at the given path, or transpiles it if it hasn't been transpiled yet. /// 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. /// 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))] #[tracing::instrument(level = "trace", skip(self, handler))]
pub(super) fn get_or_transpile_function( pub(super) fn get_or_transpile_function(
&mut self, &mut self,
@ -50,13 +55,6 @@ impl Transpiler {
) -> TranspileResult<(String, TranspiledFunctionArguments)> { ) -> TranspileResult<(String, TranspiledFunctionArguments)> {
let program_identifier = identifier_span.source_file().identifier(); let program_identifier = identifier_span.source_file().identifier();
let function = scope.get_variable(identifier_span.str()); 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 function_data = function.ok_or_else(|| {
let err = TranspileError::MissingFunctionDeclaration( let err = TranspileError::MissingFunctionDeclaration(
@ -66,150 +64,246 @@ impl Transpiler {
err err
})?; })?;
let VariableData::Function { let (function_data, function_scope, function_type) = function_data
function_data, .as_ref()
path: function_path, .as_function()
function_scope, .expect("called variable should be of type function");
} = function_data.as_ref()
else {
unreachable!("must be of correct type, otherwise errored out before");
};
if !already_transpiled { match function_type {
tracing::trace!("Function not transpiled yet, transpiling."); 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 function_location = function_path
|| { .get()
let hash_data = program_identifier.to_string() + "\0" + identifier_span.str(); .ok_or_else(|| {
Ok("shu/".to_string() + &md5::hash(hash_data).to_hex_lowercase()) let err = TranspileError::MissingFunctionDeclaration(
}, MissingFunctionDeclaration::from_scope(identifier_span.clone(), scope),
|val| match val { );
TranspileAnnotationValue::None(_) => Ok(identifier_span.str().to_string()), handler.receive(Box::new(err.clone()));
TranspileAnnotationValue::Expression(expr, _) => expr err
.comptime_eval(scope, handler) })
.ok() .map(String::to_owned)?;
.and_then(|val| val.to_string_no_macro())
.ok_or_else(|| { let args = self.transpile_function_arguments(
let err = TranspileError::IllegalAnnotationContent( function_data,
IllegalAnnotationContent { &function_location,
annotation: identifier_span.clone(), arguments,
message: "Cannot evaluate annotation at compile time" scope,
.to_string(), handler,
}, )?;
);
handler.receive(Box::new(err.clone())); Ok((function_location, args))
err }
}), FunctionVariableDataType::ComptimeArguments { function_paths } => {
TranspileAnnotationValue::Map(_, span) => { 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 = let err =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent { TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: span.clone(), annotation: identifier_span.clone(),
message: "Deobfuscate annotation cannot be a map.".to_string(), message: "Cannot evaluate annotation at compile time".to_string(),
}); });
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) err
} }),
}, TranspileAnnotationValue::Map(_, span) => {
)?; let err = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: span.clone(),
let function_location = format!( message: "Deobfuscate annotation cannot be a map.".to_string(),
"{namespace}:{modified_name}", });
namespace = function_data.namespace handler.receive(Box::new(err.clone()));
); Err(err)
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(),
},
);
}
} }
} },
)?;
let commands = if let Some(suffix) = function_suffix {
self.transpile_function(&statements, program_identifier, function_scope, handler)?; 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 function_location = format!(
let err = TranspileError::ConflictingFunctionNames(ConflictingFunctionNames { "{namespace}:{modified_name}",
name: modified_name, namespace = function_data.namespace
definition: identifier_span.clone(), );
});
handler.receive(Box::new(err.clone()));
return Err(err);
}
let function = namespace.function_mut(&modified_name); set_function_path(function_location.clone());
function.get_commands_mut().extend(commands);
if function_data.annotations.contains_key("tick") { for (i, (param, comptime_arg)) in function_data
self.datapack.add_tick(&function_location); .parameters
} .iter()
if function_data.annotations.contains_key("load") { .zip(comptime_args)
self.datapack.add_load(&function_location); .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 let commands =
.get() self.transpile_function(&statements, program_identifier, function_scope, handler)?;
.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( let namespace = self.datapack.namespace_mut(&function_data.namespace);
function_data,
&function_location,
arguments,
scope,
handler,
)?;
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( fn transpile_function(
@ -238,6 +332,51 @@ impl Transpiler {
Ok(commands) 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)] #[expect(clippy::too_many_lines)]
fn transpile_function_arguments( fn transpile_function_arguments(
&mut self, &mut self,
@ -264,6 +403,7 @@ impl Transpiler {
Some(arg_count) if arg_count > 0 => { Some(arg_count) if arg_count > 0 => {
#[derive(Debug, Clone, EnumAsInner)] #[derive(Debug, Clone, EnumAsInner)]
enum Parameter { enum Parameter {
Comptime,
Static(MacroString), Static(MacroString),
Storage { Storage {
prepare_cmds: Vec<Command>, prepare_cmds: Vec<Command>,
@ -275,64 +415,119 @@ impl Transpiler {
let mut compiled_args = Vec::<Parameter>::new(); let mut compiled_args = Vec::<Parameter>::new();
let mut errs = Vec::new(); let mut errs = Vec::new();
for expression in arguments.iter().flat_map(|expressions| expressions.iter()) { for (expression, is_comptime) in arguments
let value = match expression { .iter()
Expression::Primary(Primary::Lua(lua)) => { .flat_map(|expressions| expressions.iter())
lua.eval_comptime(scope, handler).and_then(|val| match val { .zip(
Ok(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)), parameters
Ok(val) => Ok(Parameter::Static(val.to_macro_string())), .iter()
Err(err) => { .map(|p| matches!(p.variable_type(), FunctionVariableType::Value(_))),
let err = TranspileError::NotComptime(err); )
handler.receive(Box::new(err.clone())); {
Err(err) let value = if is_comptime {
} Ok(Parameter::Comptime)
}) } else {
} match expression {
Expression::Primary(Primary::Integer(num)) => { Expression::Primary(Primary::Lua(lua)) => {
Ok(Parameter::Static(num.span.str().to_string().into())) lua.eval_comptime(scope, handler).and_then(|val| match val {
} Ok(ComptimeValue::MacroString(s)) => Ok(Parameter::Static(s)),
Expression::Primary(Primary::Boolean(bool)) => { Ok(val) => Ok(Parameter::Static(val.to_macro_string())),
Ok(Parameter::Static(bool.span.str().to_string().into())) Err(err) => {
} let err = TranspileError::NotComptime(err);
Expression::Primary(Primary::StringLiteral(string)) => { handler.receive(Box::new(err.clone()));
Ok(Parameter::Static(string.str_content().to_string().into())) Err(err)
} }
Expression::Primary(Primary::MacroStringLiteral(literal)) => { })
Ok(Parameter::Static(literal.into())) }
} Expression::Primary(Primary::Integer(num)) => {
Expression::Primary(primary @ Primary::Identifier(ident)) => { Ok(Parameter::Static(num.span.str().to_string().into()))
let var = scope.get_variable(ident.span.str()).ok_or_else(|| { }
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { Expression::Primary(Primary::Boolean(bool)) => {
identifier: ident.span(), Ok(Parameter::Static(bool.span.str().to_string().into()))
}); }
handler.receive(Box::new(err.clone())); Expression::Primary(Primary::StringLiteral(string)) => {
err Ok(Parameter::Static(string.str_content().to_string().into()))
})?; }
match var.as_ref() { Expression::Primary(Primary::MacroStringLiteral(literal)) => {
VariableData::MacroParameter { macro_name, .. } => { Ok(Parameter::Static(literal.into()))
Ok(Parameter::Static(MacroString::MacroString(vec![ }
MacroStringPart::MacroUsage(macro_name.clone()), 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::BooleanStorage { .. }
| VariableData::ScoreboardValue { .. } => { | VariableData::ScoreboardValue { .. } => {
let (temp_storage, [temp_path]) = let (temp_storage, [temp_path]) =
self.get_temp_storage_locations_array(); self.get_temp_storage_locations_array();
let prepare_cmds = self.transpile_primary_expression( let prepare_cmds = self.transpile_primary_expression(
primary, primary,
&super::expression::DataLocation::Storage { &super::expression::DataLocation::Storage {
storage_name: temp_storage.clone(), storage_name: temp_storage.clone(),
path: temp_path.clone(), path: temp_path.clone(),
r#type: match var.as_ref() { r#type: match var.as_ref() {
VariableData::BooleanStorage { .. } => { VariableData::BooleanStorage { .. } => {
StorageType::Boolean StorageType::Boolean
} }
VariableData::ScoreboardValue { .. } => { VariableData::ScoreboardValue { .. } => {
StorageType::Int StorageType::Int
} }
_ => unreachable!("checked in parent match"), _ => 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, scope,
handler, handler,
@ -340,39 +535,25 @@ impl Transpiler {
Ok(Parameter::Storage { Ok(Parameter::Storage {
prepare_cmds, prepare_cmds,
storage_name: temp_storage, storage_name,
path: temp_path, 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(
Expression::Primary(Primary::MemberAccess(member_access)) => { Primary::Parenthesized(_)
if let Ok(value) = member_access.parent().comptime_member_access( | Primary::Prefix(_)
member_access, | Primary::Indexed(_)
scope, | Primary::FunctionCall(_),
handler, )
) { | Expression::Binary(_) => {
Ok(Parameter::Static(value.to_macro_string())) let (temp_storage, [temp_path]) =
} else {
let (storage_name, [path]) =
self.get_temp_storage_locations_array(); self.get_temp_storage_locations_array();
let prepare_cmds = self.transpile_expression( let prepare_cmds = self.transpile_expression(
expression, expression,
&super::expression::DataLocation::Storage { &super::expression::DataLocation::Storage {
storage_name: storage_name.clone(), storage_name: temp_storage.clone(),
path: path.clone(), path: temp_path.clone(),
r#type: StorageType::Int, r#type: StorageType::Int,
}, },
scope, scope,
@ -381,37 +562,11 @@ impl Transpiler {
Ok(Parameter::Storage { Ok(Parameter::Storage {
prepare_cmds, prepare_cmds,
storage_name, storage_name: temp_storage,
path, 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 { match value {
@ -437,6 +592,7 @@ impl Transpiler {
FunctionVariableType::Macro(_) => { FunctionVariableType::Macro(_) => {
let arg_name = crate::util::identifier_to_macro(param.identifier().span.str()); let arg_name = crate::util::identifier_to_macro(param.identifier().span.str());
match data { match data {
Parameter::Comptime => {}
Parameter::Static(s) => { Parameter::Static(s) => {
match s { match s {
MacroString::String(value) => statics.insert( MacroString::String(value) => statics.insert(
@ -469,6 +625,7 @@ impl Transpiler {
let target = crate::util::identifier_to_scoreboard_target(param_str); let target = crate::util::identifier_to_scoreboard_target(param_str);
match data { match data {
Parameter::Comptime => {}
Parameter::Static(s) => { Parameter::Static(s) => {
match s.as_str() { match s.as_str() {
Ok(s) => { Ok(s) => {
@ -502,6 +659,7 @@ impl Transpiler {
let target_path = crate::util::identifier_to_scoreboard_target(param_str); let target_path = crate::util::identifier_to_scoreboard_target(param_str);
match data { match data {
Parameter::Comptime => {}
Parameter::Static(s) => { Parameter::Static(s) => {
match s.as_str() { match s.as_str() {
Ok(s) => { 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)}, (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>, handler: &impl Handler<base::Error>,
) -> TranspileResult<((), ())> { ) -> TranspileResult<((), ())> {
let _ = scope; let _ = scope;
handler.receive(TranspileError::LuaDisabled); handler.receive(Box::new(TranspileError::LuaDisabled));
tracing::error!("Lua code evaluation is disabled"); tracing::error!("Lua code evaluation is disabled");
Err(TranspileError::LuaDisabled) Err(TranspileError::LuaDisabled)
} }
@ -440,9 +440,9 @@ mod disabled {
&self, &self,
scope: &Arc<Scope>, scope: &Arc<Scope>,
handler: &impl Handler<base::Error>, handler: &impl Handler<base::Error>,
) -> TranspileResult<Option<ComptimeValue>> { ) -> TranspileResult<Result<ComptimeValue, crate::transpile::error::NotComptime>> {
let _ = scope; let _ = scope;
handler.receive(TranspileError::LuaDisabled); handler.receive(Box::new(TranspileError::LuaDisabled));
tracing::error!("Lua code evaluation is disabled"); tracing::error!("Lua code evaluation is disabled");
Err(TranspileError::LuaDisabled) Err(TranspileError::LuaDisabled)
} }

View File

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

View File

@ -1,9 +1,9 @@
//! Transpiler for `Shulkerscript` //! Transpiler for `Shulkerscript`
use std::{ use std::{
collections::{BTreeMap, BTreeSet, HashSet}, collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ops::Deref, ops::Deref,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock, RwLock},
}; };
use itertools::Itertools; use itertools::Itertools;
@ -13,7 +13,7 @@ use crate::{
base::{self, source_file::SourceElement, Handler}, base::{self, source_file::SourceElement, Handler},
semantic::error::UnexpectedExpression, semantic::error::UnexpectedExpression,
syntax::syntax_tree::{ syntax::syntax_tree::{
declaration::{Declaration, ImportItems}, declaration::{Declaration, FunctionVariableType, ImportItems},
expression::{Expression, FunctionCall, PrefixOperator, Primary}, expression::{Expression, FunctionCall, PrefixOperator, Primary},
program::{Namespace, ProgramFile}, program::{Namespace, ProgramFile},
statement::{ statement::{
@ -25,6 +25,7 @@ use crate::{
transpile::{ transpile::{
error::IllegalAnnotationContent, error::IllegalAnnotationContent,
util::{MacroString, MacroStringPart}, util::{MacroString, MacroStringPart},
variables::FunctionVariableDataType,
}, },
}; };
@ -240,6 +241,7 @@ impl Transpiler {
} }
/// Transpiles the given declaration. /// Transpiles the given declaration.
#[expect(clippy::too_many_lines)]
fn transpile_declaration( fn transpile_declaration(
&mut self, &mut self,
declaration: &Declaration, declaration: &Declaration,
@ -283,12 +285,25 @@ impl Transpiler {
public: function.is_public(), public: function.is_public(),
annotations, 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( scope.set_variable(
&name, &name,
VariableData::Function { VariableData::Function {
function_data, function_data,
path: OnceLock::new(), function_scope,
function_scope: Scope::with_parent(scope.clone()), 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 = let import_identifier =
super::util::calculate_import_identifier(&program_identifier, path); super::util::calculate_import_identifier(&program_identifier, path);
// let aliases = &mut self.aliases;
match import.items() { match import.items() {
ImportItems::All(_) => { ImportItems::All(_) => {
handler.receive(base::Error::Other( handler.receive(base::Error::Other(

View File

@ -47,10 +47,10 @@ pub enum VariableData {
Function { Function {
/// The function data. /// The function data.
function_data: FunctionData, function_data: FunctionData,
/// The path to the function once it is generated.
path: OnceLock<String>,
/// The scope of the function. /// The scope of the function.
function_scope: Arc<Scope>, function_scope: Arc<Scope>,
/// The variable data.
variable_data: FunctionVariableDataType,
}, },
/// A macro function parameter. /// A macro function parameter.
MacroParameter { 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)] #[derive(Debug, Clone, Copy, EnumAsInner)]
pub enum TranspileAssignmentTarget<'a> { pub enum TranspileAssignmentTarget<'a> {
Identifier(&'a Identifier), 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. /// Does only strip invalid characters if the `shulkerbox` feature is not enabled.
#[cfg(not(feature = "shulkerbox"))] #[cfg(not(feature = "shulkerbox"))]
#[must_use] #[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("__") if ident.contains("__")
|| ident || ident
.chars() .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. /// Does only strip invalid characters if the `shulkerbox` feature is not enabled.
#[cfg(not(feature = "shulkerbox"))] #[cfg(not(feature = "shulkerbox"))]
#[must_use] #[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()) if !(..=16).contains(&ident.len())
|| ident || ident
.chars() .chars()