Compare commits

...

3 Commits

Author SHA1 Message Date
Moritz Hölting 0fd9dc432e first version of print internal function for easier displaying variable values 2025-03-16 23:26:20 +01:00
Moritz Hölting 237207a447 implement tag and array variables 2025-03-16 20:46:18 +01:00
Moritz Hölting b119ca33c7 implement todos in scoreboard variable 2025-03-16 19:25:25 +01:00
13 changed files with 1173 additions and 336 deletions

View File

@ -18,8 +18,8 @@ license = "MIT OR Apache-2.0"
default = ["fs_access", "lua", "shulkerbox", "zip"]
fs_access = ["shulkerbox?/fs_access"]
lua = ["dep:mlua"]
serde = ["dep:serde", "dep:flexbuffers", "shulkerbox?/serde"]
shulkerbox = ["dep:shulkerbox", "dep:chksum-md5"]
serde = ["dep:serde", "dep:serde_json", "shulkerbox?/serde"]
shulkerbox = ["dep:shulkerbox", "dep:chksum-md5", "dep:serde_json"]
zip = ["shulkerbox?/zip"]
[dependencies]
@ -28,12 +28,12 @@ chksum-md5 = { version = "0.1.0", optional = true }
colored = "3.0.0"
derive_more = { version = "2.0.1", default-features = false, features = ["deref", "deref_mut", "from"] }
enum-as-inner = "0.6.0"
flexbuffers = { version = "25.2.10", optional = true }
getset = "0.1.2"
itertools = "0.14.0"
mlua = { version = "0.10.2", features = ["lua54", "vendored"], optional = true }
pathdiff = "0.2.3"
serde = { version = "1.0.217", features = ["derive"], optional = true }
serde_json = { version = "1.0.138", optional = true }
# shulkerbox = { version = "0.1.0", default-features = false, optional = true }
shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "e9f2b9b91d72322ec2e063ce7b83415071306468", default-features = false, optional = true }
strsim = "0.11.1"

View File

@ -33,6 +33,7 @@ pub enum Error {
IncompatibleFunctionAnnotation(#[from] IncompatibleFunctionAnnotation),
}
// TODO: remove duplicate error (also in transpile)
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct MissingFunctionDeclaration {
@ -43,6 +44,7 @@ pub struct MissingFunctionDeclaration {
}
impl MissingFunctionDeclaration {
#[expect(dead_code)]
pub(super) fn from_context(identifier_span: Span, functions: &HashSet<String>) -> Self {
let own_name = identifier_span.str();
let alternatives = functions

View File

@ -4,14 +4,11 @@
use std::collections::HashSet;
use error::{
IncompatibleFunctionAnnotation, InvalidNamespaceName, MissingFunctionDeclaration,
UnresolvedMacroUsage,
};
use error::{IncompatibleFunctionAnnotation, InvalidNamespaceName};
use crate::{
base::{self, source_file::SourceElement as _, Handler},
lexical::token::{MacroStringLiteral, MacroStringLiteralPart},
lexical::token::MacroStringLiteral,
syntax::syntax_tree::{
declaration::{Declaration, Function, ImportItems},
expression::{Expression, FunctionCall, Parenthesized, Primary},
@ -387,28 +384,29 @@ impl MacroStringLiteral {
/// Analyzes the semantics of the macro string literal.
pub fn analyze_semantics(
&self,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
_macro_names: &HashSet<String>,
_handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let mut errors = Vec::new();
for part in self.parts() {
if let MacroStringLiteralPart::MacroUsage { identifier, .. } = part {
if !macro_names.contains(identifier.span.str()) {
let err = error::Error::UnresolvedMacroUsage(UnresolvedMacroUsage {
span: identifier.span(),
});
handler.receive(err.clone());
errors.push(err);
}
}
}
// let mut errors = Vec::new();
// TODO: allow macro string literals to also contain other variables
// for part in self.parts() {
// if let MacroStringLiteralPart::MacroUsage { identifier, .. } = part {
// if !macro_names.contains(identifier.span.str()) {
// let err = error::Error::UnresolvedMacroUsage(UnresolvedMacroUsage {
// span: identifier.span(),
// });
// handler.receive(err.clone());
// errors.push(err);
// }
// }
// }
#[expect(clippy::option_if_let_else)]
if let Some(err) = errors.first() {
Err(err.clone())
} else {
Ok(())
}
// #[expect(clippy::option_if_let_else)]
// if let Some(err) = errors.first() {
// Err(err.clone())
// } else {
Ok(())
// }
}
}
@ -462,13 +460,14 @@ impl FunctionCall {
) -> Result<(), error::Error> {
let mut errors = Vec::new();
if !function_names.contains(self.identifier().span.str()) {
let err = error::Error::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_context(self.identifier().span(), function_names),
);
handler.receive(err.clone());
errors.push(err);
}
// TODO: also check for internal functions
// if !function_names.contains(self.identifier().span.str()) {
// let err = error::Error::MissingFunctionDeclaration(
// MissingFunctionDeclaration::from_context(self.identifier().span(), function_names),
// );
// handler.receive(err.clone());
// errors.push(err);
// }
for expression in self
.arguments()

View File

@ -42,7 +42,7 @@ where
// hold guard so no other can serialize at the same time in same thread
let s = DEDUPLICATE_SOURCE_FILES.with(|d| {
let guard = d.read().unwrap();
let mut serialized_data = flexbuffers::FlexbufferSerializer::new();
let mut serialized_data = serde_json::Serializer::new(Vec::new());
self.0
.serialize(&mut serialized_data)
.map_err(|_| serde::ser::Error::custom("could not buffer serialization"))?;

View File

@ -545,6 +545,20 @@ impl<'a> Parser<'a> {
arguments: token_tree.list,
}))
}
Reading::IntoDelimited(punc) if punc.punctuation == '[' => {
let token_tree = self.step_into(
Delimiter::Bracket,
|p| p.parse_expression(handler),
handler,
)?;
Ok(Primary::Indexed(Indexed {
object: Box::new(Primary::Identifier(identifier)),
left_bracket: token_tree.open,
index: Box::new(token_tree.tree?),
right_bracket: token_tree.close,
}))
}
_ => {
// regular identifier
Ok(Primary::Identifier(identifier))

View File

@ -572,7 +572,7 @@ pub struct ScoreVariableDeclaration {
close_bracket: Punctuation,
/// The optional assignment of the variable.
#[get = "pub"]
target_assignment: Option<(AnyStringLiteral, VariableDeclarationAssignment)>,
assignment: Option<VariableDeclarationAssignment>,
/// The annotations of the variable declaration.
#[get = "pub"]
annotations: VecDeque<Annotation>,
@ -582,17 +582,18 @@ impl SourceElement for ScoreVariableDeclaration {
fn span(&self) -> Span {
self.int_keyword
.span()
.join(&self.target_assignment.as_ref().map_or_else(
|| self.close_bracket.span(),
|(_, assignment)| assignment.span(),
))
.join(
&self
.assignment
.as_ref()
.map_or_else(|| self.close_bracket.span(), SourceElement::span),
)
.expect("The span of the score variable declaration is invalid.")
}
}
impl ScoreVariableDeclaration {
/// Dissolves the [`ScoreVariableDeclaration`] into its components.
#[expect(clippy::type_complexity)]
#[must_use]
pub fn dissolve(
self,
@ -602,7 +603,7 @@ impl ScoreVariableDeclaration {
Identifier,
Punctuation,
Punctuation,
Option<(AnyStringLiteral, VariableDeclarationAssignment)>,
Option<VariableDeclarationAssignment>,
) {
(
self.int_keyword,
@ -610,7 +611,7 @@ impl ScoreVariableDeclaration {
self.identifier,
self.open_bracket,
self.close_bracket,
self.target_assignment,
self.assignment,
)
}
}
@ -939,9 +940,9 @@ impl<'a> Parser<'a> {
&mut self,
handler: &impl Handler<base::Error>,
) -> ParseResult<VariableDeclaration> {
#[derive(Debug, Clone)]
enum IndexingType {
IntegerSize(Integer),
AnyString(AnyStringLiteral),
None,
}
@ -992,27 +993,13 @@ impl<'a> Parser<'a> {
IndexingType::IntegerSize(int)
}
Reading::Atomic(Token::StringLiteral(s)) => {
let selector = AnyStringLiteral::from(s);
p.forward();
IndexingType::AnyString(selector)
}
Reading::Atomic(Token::MacroStringLiteral(s)) => {
let selector = AnyStringLiteral::from(s);
p.forward();
IndexingType::AnyString(selector)
}
Reading::DelimitedEnd(punc) if punc.punctuation == ']' => {
IndexingType::None
}
unexpected => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::Either(&[
SyntaxKind::Integer,
SyntaxKind::AnyStringLiteral,
]),
expected: SyntaxKind::Integer,
found: unexpected.into_token(),
});
handler.receive(err.clone());
@ -1034,10 +1021,10 @@ impl<'a> Parser<'a> {
let assignment = self
.try_parse(|p| {
// read equals sign
let equals = p.parse_punctuation('=', true, handler)?;
let equals = p.parse_punctuation('=', true, &VoidHandler)?;
// read expression
let expression = p.parse_expression(handler)?;
let expression = p.parse_expression(&VoidHandler)?;
Ok(VariableDeclarationAssignment { equals, expression })
})
@ -1053,37 +1040,6 @@ impl<'a> Parser<'a> {
annotations: VecDeque::new(),
}))
}
IndexingType::AnyString(selector) => {
let equals = self.parse_punctuation('=', true, handler)?;
let expression = self.parse_expression(handler)?;
let assignment = VariableDeclarationAssignment { equals, expression };
match variable_type.keyword {
KeywordKind::Int => {
Ok(VariableDeclaration::Score(ScoreVariableDeclaration {
int_keyword: variable_type,
criteria: criteria_selection,
identifier,
open_bracket,
close_bracket,
target_assignment: Some((selector, assignment)),
annotations: VecDeque::new(),
}))
}
KeywordKind::Bool => {
Ok(VariableDeclaration::Tag(TagVariableDeclaration {
bool_keyword: variable_type,
identifier,
open_bracket,
close_bracket,
target_assignment: Some((selector, assignment)),
annotations: VecDeque::new(),
}))
}
_ => unreachable!(),
}
}
IndexingType::None => match variable_type.keyword {
KeywordKind::Int => {
Ok(VariableDeclaration::Score(ScoreVariableDeclaration {
@ -1092,7 +1048,7 @@ impl<'a> Parser<'a> {
identifier,
open_bracket,
close_bracket,
target_assignment: None,
assignment: None,
annotations: VecDeque::new(),
}))
}

View File

@ -42,6 +42,8 @@ pub enum TranspileError {
UnknownIdentifier(#[from] UnknownIdentifier),
#[error(transparent)]
MissingValue(#[from] MissingValue),
#[error(transparent)]
IllegalIndexing(#[from] IllegalIndexing),
}
/// The result of a transpilation operation.
@ -309,3 +311,54 @@ impl Display for MissingValue {
}
impl std::error::Error for MissingValue {}
/// An error that occurs when an indexing operation is not permitted.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IllegalIndexing {
pub reason: IllegalIndexingReason,
pub expression: Span,
}
impl Display for IllegalIndexing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", Message::new(Severity::Error, &self.reason))?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.expression, Option::<u8>::None)
)
}
}
impl std::error::Error for IllegalIndexing {}
/// The reason why an indexing operation is not permitted.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IllegalIndexingReason {
NotIdentifier,
InvalidComptimeType { expected: ExpectedType },
IndexOutOfBounds { index: usize, length: usize },
}
impl Display for IllegalIndexingReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotIdentifier => {
write!(f, "The expression is not an identifier.")
}
Self::InvalidComptimeType { expected } => {
write!(
f,
"The expression can only be indexed with type {expected} that can be evaluated at compile time."
)
}
Self::IndexOutOfBounds { index, length } => {
write!(
f,
"The index {index} is out of bounds for the expression with length {length}."
)
}
}
}
}

View File

@ -19,7 +19,7 @@ use shulkerbox::prelude::{Command, Condition, Execute};
#[cfg(feature = "shulkerbox")]
use super::{
error::{MismatchedTypes, UnknownIdentifier},
error::{IllegalIndexing, IllegalIndexingReason, MismatchedTypes, UnknownIdentifier},
TranspileResult, Transpiler,
};
#[cfg(feature = "shulkerbox")]
@ -31,7 +31,6 @@ use crate::{
},
};
// TODO: fix this leading to compile errors without 'shulkerbox' feature
/// Compile-time evaluated value
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
@ -42,17 +41,6 @@ pub enum ComptimeValue {
MacroString(MacroString),
}
impl Display for ComptimeValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Boolean(boolean) => write!(f, "{boolean}"),
Self::Integer(int) => write!(f, "{int}"),
Self::String(string) => write!(f, "{string}"),
Self::MacroString(macro_string) => write!(f, "{macro_string}"),
}
}
}
impl ComptimeValue {
/// Returns the value as a string not containing a macro.
#[must_use]
@ -64,6 +52,17 @@ impl ComptimeValue {
Self::MacroString(_) => None,
}
}
/// Returns the value as a [`MacroString`].
#[must_use]
pub fn to_macro_string(&self) -> MacroString {
match self {
Self::Boolean(boolean) => MacroString::String(boolean.to_string()),
Self::Integer(int) => MacroString::String(int.to_string()),
Self::String(string) => MacroString::String(string.clone()),
Self::MacroString(macro_string) => macro_string.clone(),
}
}
}
/// The type of an expression.
@ -287,27 +286,29 @@ impl Primary {
}
},
Self::Indexed(indexed) => {
let Self::Identifier(ident) = indexed.object().as_ref() else {
todo!("throw error: cannot index anything except identifiers")
};
scope
.get_variable(ident.span.str())
.map_or(false, |variable| match r#type {
ValueType::Boolean => {
matches!(
variable.as_ref(),
VariableData::Tag { .. } | VariableData::BooleanStorageArray { .. }
)
}
ValueType::Integer => {
matches!(
variable.as_ref(),
VariableData::Scoreboard { .. }
| VariableData::ScoreboardArray { .. }
)
}
ValueType::String => false,
})
if let Self::Identifier(ident) = indexed.object().as_ref() {
scope
.get_variable(ident.span.str())
.map_or(false, |variable| match r#type {
ValueType::Boolean => {
matches!(
variable.as_ref(),
VariableData::Tag { .. }
| VariableData::BooleanStorageArray { .. }
)
}
ValueType::Integer => {
matches!(
variable.as_ref(),
VariableData::Scoreboard { .. }
| VariableData::ScoreboardArray { .. }
)
}
ValueType::String => false,
})
} else {
false
}
}
#[cfg_attr(not(feature = "lua"), expect(unused_variables))]
Self::Lua(lua) => {
@ -785,9 +786,16 @@ impl Transpiler {
}
}
Primary::Indexed(indexed) => {
let Primary::Identifier(ident) = indexed.object().as_ref() else {
todo!("can only index identifier")
};
let ident = if let Primary::Identifier(ident) = indexed.object().as_ref() {
Ok(ident)
} else {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::NotIdentifier,
expression: indexed.object().span(),
});
handler.receive(err.clone());
Err(err)
}?;
let variable = scope.get_variable(ident.span.str());
#[expect(clippy::option_if_let_else)]
if let Some(variable) = variable.as_deref() {
@ -801,7 +809,14 @@ impl Transpiler {
target,
})
} else {
todo!("can only index scoreboard with comptime string")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
}
VariableData::ScoreboardArray { objective, targets } => {
@ -818,10 +833,25 @@ impl Transpiler {
target,
})
} else {
todo!("index out of bounds")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::IndexOutOfBounds {
length: targets.len(),
index: usize::try_from(index).unwrap_or(usize::MAX),
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
} else {
todo!("can only index array with comptime integer")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
}
VariableData::BooleanStorageArray {
@ -842,10 +872,25 @@ impl Transpiler {
r#type: StorageType::Boolean,
})
} else {
todo!("index out of bounds")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::IndexOutOfBounds {
length: paths.len(),
index: usize::try_from(index).unwrap_or(usize::MAX),
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
} else {
todo!("can only index array with comptime integer")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
}
_ => {
@ -1022,9 +1067,16 @@ impl Transpiler {
self.transpile_expression_as_condition(parenthesized.expression(), scope, handler)
}
Primary::Indexed(indexed) => {
let Primary::Identifier(ident) = indexed.object().as_ref() else {
todo!("can only index identifier")
};
let ident = if let Primary::Identifier(ident) = indexed.object().as_ref() {
Ok(ident)
} else {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::NotIdentifier,
expression: indexed.object().span(),
});
handler.receive(err.clone());
Err(err)
}?;
#[expect(clippy::option_if_let_else)]
if let Some(variable) = scope.get_variable(ident.span.str()).as_deref() {
#[expect(clippy::single_match_else)]
@ -1049,10 +1101,25 @@ impl Transpiler {
)),
))
} else {
todo!("index out of bounds")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::IndexOutOfBounds {
length: paths.len(),
index: usize::try_from(index).unwrap_or(usize::MAX),
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
} else {
todo!("can only index array with comptime integer")
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: indexed.index().span(),
});
handler.receive(err.clone());
Err(err)
}
}
_ => {

View File

@ -0,0 +1,317 @@
//! Functions provided by the language itself.
use std::{
ops::{Bound, Deref, RangeBounds},
sync::Arc,
};
use shulkerbox::prelude::Command;
use serde_json::{json, Value as JsonValue};
use crate::{
base::{source_file::SourceElement as _, VoidHandler},
lexical::token::{Identifier, MacroStringLiteralPart},
semantic::error::InvalidFunctionArguments,
syntax::syntax_tree::expression::{Expression, FunctionCall, Primary},
transpile::{
error::{IllegalIndexing, IllegalIndexingReason, LuaRuntimeError, UnknownIdentifier},
expression::{ComptimeValue, DataLocation, StorageType},
util::MacroString,
TranspileError,
},
};
use super::{Scope, TranspileResult, Transpiler, VariableData};
/// A function that can be called from the language.
pub type InternalFunction =
fn(&mut Transpiler, &Arc<Scope>, &FunctionCall) -> TranspileResult<Vec<Command>>;
/// Adds all internal functions to the scope.
pub fn add_all_to_scope(scope: &Arc<Scope>) {
scope.set_variable(
"print",
VariableData::InternalFunction {
implementation: print_function,
},
);
}
fn get_args_assert_in_range(
call: &FunctionCall,
range: impl RangeBounds<usize>,
) -> TranspileResult<Vec<&Expression>> {
let args = call
.arguments()
.as_ref()
.map(|args| args.elements().map(Deref::deref).collect::<Vec<_>>())
.unwrap_or_default();
if range.contains(&args.len()) {
Ok(args)
} else {
let span = args
.first()
.and_then(|first| {
args.last()
.map(|last| first.span().join(&last.span()).expect("invalid span"))
})
.unwrap_or_else(|| {
call.left_parenthesis()
.span()
.join(&call.right_parenthesis().span())
.expect("invalid span")
});
let actual = args.len();
let expected = match range.start_bound() {
Bound::Excluded(excluded) => (excluded + 1 > actual).then_some(excluded + 1),
Bound::Included(&included) => (included > actual).then_some(included),
Bound::Unbounded => None,
}
.or_else(|| match range.end_bound() {
Bound::Excluded(&excluded) => (excluded <= actual).then_some(excluded.wrapping_sub(1)),
Bound::Included(&included) => (included < actual).then_some(included),
Bound::Unbounded => None,
})
.unwrap_or_default();
Err(TranspileError::InvalidFunctionArguments(
InvalidFunctionArguments {
expected,
actual: args.len(),
span,
},
))
}
}
#[expect(clippy::too_many_lines)]
fn print_function(
transpiler: &mut Transpiler,
scope: &Arc<Scope>,
call: &FunctionCall,
) -> TranspileResult<Vec<Command>> {
const PARAM_COLOR: &str = "gray";
#[expect(clippy::option_if_let_else)]
fn get_identifier_part(
ident: &Identifier,
_transpiler: &mut Transpiler,
scope: &Arc<Scope>,
) -> TranspileResult<(bool, Vec<Command>, JsonValue)> {
if let Some(var) = scope.get_variable(ident.span.str()).as_deref() {
match var {
VariableData::MacroParameter { macro_name, .. } => Ok((
true,
Vec::new(),
json!({"text": format!("$({macro_name})"), "color": PARAM_COLOR}),
)),
VariableData::ScoreboardValue { objective, target } => Ok((
false,
Vec::new(),
get_data_location(&DataLocation::ScoreboardValue {
objective: objective.to_string(),
target: target.to_string(),
}),
)),
VariableData::BooleanStorage { storage_name, path } => Ok((
false,
Vec::new(),
get_data_location(&DataLocation::Storage {
storage_name: storage_name.to_string(),
path: path.to_string(),
r#type: StorageType::Boolean,
}),
)),
_ => todo!("get_identifier_part"),
}
} else {
Err(TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: ident.span(),
}))
}
}
fn get_data_location(location: &DataLocation) -> JsonValue {
match location {
DataLocation::ScoreboardValue { objective, target } => {
json!({"score": {"name": target, "objective": objective}, "color": PARAM_COLOR})
}
DataLocation::Storage {
storage_name, path, ..
} => json!({"nbt": path, "storage": storage_name, "color": PARAM_COLOR}),
DataLocation::Tag { .. } => todo!("implement tag"),
}
}
let args = get_args_assert_in_range(call, 1..=2)?;
let first = args.first().expect("checked range");
let (target, message_expression) = args.get(1).map_or_else(
|| ("@a".into(), first),
|second| {
(
first
.comptime_eval(scope, &VoidHandler)
.map_or_else(|| "@a".into(), |val| val.to_macro_string()),
second,
)
},
);
let mut contains_macro = matches!(target, MacroString::MacroString(_));
let (mut cmds, parts) = match message_expression {
Expression::Primary(primary) => match primary {
Primary::Boolean(boolean) => Ok((
Vec::new(),
vec![JsonValue::String(boolean.value().to_string())],
)),
Primary::Integer(integer) => Ok((
Vec::new(),
vec![JsonValue::String(integer.as_i64().to_string())],
)),
Primary::StringLiteral(string) => Ok((
Vec::new(),
vec![JsonValue::String(string.str_content().to_string())],
)),
Primary::Lua(lua) => {
let (ret, _lua) = lua.eval(scope, &VoidHandler)?;
Ok((
Vec::new(),
vec![JsonValue::String(ret.to_string().map_err(|err| {
TranspileError::LuaRuntimeError(LuaRuntimeError::from_lua_err(
&err,
lua.span(),
))
})?)],
))
}
Primary::Identifier(ident) => {
// TODO: get_identifier_part
let (cur_contains_macro, cmds, part) =
get_identifier_part(ident, transpiler, scope).expect("failed");
contains_macro |= cur_contains_macro;
Ok((cmds, vec![part]))
}
Primary::Indexed(indexed) => match indexed.object().as_ref() {
Primary::Identifier(ident) => {
match scope.get_variable(ident.span.str()).as_deref() {
Some(VariableData::Scoreboard { objective }) => {
if let Some(ComptimeValue::String(index)) =
indexed.index().comptime_eval(scope, &VoidHandler)
{
Ok((
Vec::new(),
vec![get_data_location(&DataLocation::ScoreboardValue {
objective: objective.to_string(),
target: index,
})],
))
} else {
todo!("allow macro string, but throw error when index is not constant string")
}
}
Some(VariableData::ScoreboardArray { objective, targets }) => {
if let Some(ComptimeValue::Integer(index)) =
indexed.index().comptime_eval(scope, &VoidHandler)
{
#[expect(clippy::option_if_let_else)]
if let Some(target) = usize::try_from(index)
.ok()
.and_then(|index| targets.get(index))
{
Ok((
Vec::new(),
vec![get_data_location(&DataLocation::ScoreboardValue {
objective: objective.to_string(),
target: target.to_string(),
})],
))
} else {
todo!("throw error when index is out of bounds")
}
} else {
todo!("throw error when index is not constant integer")
}
}
Some(VariableData::BooleanStorageArray {
storage_name,
paths,
}) => {
if let Some(ComptimeValue::Integer(index)) =
indexed.index().comptime_eval(scope, &VoidHandler)
{
#[expect(clippy::option_if_let_else)]
if let Some(path) = usize::try_from(index)
.ok()
.and_then(|index| paths.get(index))
{
Ok((
Vec::new(),
vec![get_data_location(&DataLocation::Storage {
storage_name: storage_name.to_string(),
path: path.to_string(),
r#type: StorageType::Boolean,
})],
))
} else {
todo!("throw error when index is out of bounds")
}
} else {
todo!("throw error when index is not constant integer")
}
}
_ => todo!(),
}
}
_ => Err(TranspileError::IllegalIndexing(IllegalIndexing {
expression: indexed.object().span(),
reason: IllegalIndexingReason::NotIdentifier,
})),
},
Primary::MacroStringLiteral(macro_string) => {
let mut cmds = Vec::new();
let mut parts = Vec::new();
for part in macro_string.parts() {
match part {
MacroStringLiteralPart::Text(text) => {
parts.push(JsonValue::String(text.str().to_string()));
}
MacroStringLiteralPart::MacroUsage { identifier, .. } => {
let (cur_contains_macro, cur_cmds, part) =
get_identifier_part(identifier, transpiler, scope)?;
contains_macro |= cur_contains_macro;
cmds.extend(cur_cmds);
parts.push(part);
}
}
}
Ok((cmds, parts))
}
_ => todo!("print_function Primary"),
},
Expression::Binary(_) => todo!("print_function Binary"),
}?;
// TODO: prepend prefix with datapack name to parts and remove following
let print_args = if parts.len() == 1 {
serde_json::to_string(&parts[0]).expect("json serialization failed")
} else {
serde_json::to_string(&parts).expect("json serialization failed")
};
// TODO: throw correct error
let cmd = format!("tellraw {target} {print_args}");
let cmd = if contains_macro {
Command::UsesMacro(cmd.parse::<MacroString>().expect("cannot fail").into())
} else {
Command::Raw(cmd)
};
cmds.push(cmd);
Ok(cmds)
}

View File

@ -4,13 +4,17 @@
mod enabled {
use std::sync::Arc;
use mlua::{Lua, Value};
use mlua::{Lua, Table, Value};
use crate::{
base::{self, source_file::SourceElement, Handler},
lexical::token::Identifier,
syntax::syntax_tree::expression::LuaCode,
transpile::{
error::{LuaRuntimeError, MismatchedTypes, TranspileError, TranspileResult},
error::{
LuaRuntimeError, MismatchedTypes, TranspileError, TranspileResult,
UnknownIdentifier,
},
expression::{ComptimeValue, ExpectedType},
Scope, VariableData,
},
@ -53,14 +57,8 @@ mod enabled {
)
};
if let Err(err) = self.add_globals(&lua, scope) {
let err = TranspileError::LuaRuntimeError(LuaRuntimeError::from_lua_err(
&err,
self.span(),
));
handler.receive(crate::Error::from(err.clone()));
return Err(err);
}
self.add_globals(&lua, scope)
.inspect_err(|err| handler.receive(err.clone()))?;
let res = lua
.load(self.code())
@ -95,111 +93,188 @@ mod enabled {
self.handle_lua_result(lua_result, handler)
}
fn add_globals(&self, lua: &Lua, scope: &Arc<Scope>) -> mlua::Result<()> {
fn add_globals(&self, lua: &Lua, scope: &Arc<Scope>) -> TranspileResult<()> {
let globals = lua.globals();
let shulkerscript_globals = {
let table = lua.create_table()?;
let (location_path, location_start, location_end) = {
let span = self.span();
let file = span.source_file();
let path = file.path().to_owned();
let start_location = span.start_location();
let end_location = span.end_location().unwrap_or_else(|| {
let line_amount = file.line_amount();
let column = file.get_line(line_amount).expect("line amount used").len();
crate::base::source_file::Location {
line: line_amount,
column,
}
});
(path, start_location, end_location)
};
table.set("file_path", location_path.to_string_lossy())?;
table.set("start_line", location_start.line)?;
table.set("start_column", location_start.column)?;
table.set("end_line", location_end.line)?;
table.set("end_column", location_end.column)?;
table.set("version", crate::VERSION)?;
table
};
let shulkerscript_globals = self
.get_std_library(lua)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
if let Some(inputs) = self.inputs() {
for x in inputs.elements() {
let name = x.span.str();
let value = match scope.get_variable(name).as_deref() {
Some(VariableData::MacroParameter { macro_name, .. }) => {
Value::String(lua.create_string(format!("$({macro_name})"))?)
}
Some(VariableData::Scoreboard { objective }) => {
let table = lua.create_table()?;
table.set("objective", objective.as_str())?;
Value::Table(table)
}
Some(VariableData::ScoreboardValue { objective, target }) => {
let table = lua.create_table()?;
table.set("objective", lua.create_string(objective)?)?;
table.set("target", lua.create_string(target)?)?;
Value::Table(table)
}
Some(VariableData::ScoreboardArray { objective, targets }) => {
let table = lua.create_table()?;
table.set("objective", objective.as_str())?;
let values = lua.create_table_from(
targets
.iter()
.enumerate()
.map(|(i, target)| (i + 1, target.as_str())),
)?;
table.set("targets", values)?;
Value::Table(table)
}
Some(VariableData::BooleanStorage { storage_name, path }) => {
let table = lua.create_table()?;
table.set("storage", lua.create_string(storage_name)?)?;
table.set("path", lua.create_string(path)?)?;
Value::Table(table)
}
Some(VariableData::BooleanStorageArray {
storage_name,
paths,
}) => {
let table = lua.create_table()?;
table.set("storage", storage_name.as_str())?;
let values = lua.create_table_from(
paths
.iter()
.enumerate()
.map(|(i, path)| (i + 1, path.as_str())),
)?;
table.set("paths", values)?;
Value::Table(table)
}
Some(VariableData::Tag { tag_name }) => {
let table = lua.create_table()?;
table.set("name", tag_name.as_str())?;
Value::Table(table)
}
Some(VariableData::Function { .. }) => {
todo!("allow function variable type")
}
None => todo!("throw correct error"),
};
globals.set(name, value)?;
for identifier in inputs.elements() {
let (name, value) = self.add_input_to_globals(identifier, lua, scope)?;
globals
.set(name, value)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
}
}
globals.set("shulkerscript", shulkerscript_globals)?;
globals
.set("shulkerscript", shulkerscript_globals)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Ok(())
}
fn get_std_library(&self, lua: &Lua) -> mlua::Result<Table> {
let table = lua.create_table()?;
let (location_path, location_start, location_end) = {
let span = self.span();
let file = span.source_file();
let path = file.path().to_owned();
let start_location = span.start_location();
let end_location = span.end_location().unwrap_or_else(|| {
let line_amount = file.line_amount();
let column = file.get_line(line_amount).expect("line amount used").len();
crate::base::source_file::Location {
line: line_amount,
column,
}
});
(path, start_location, end_location)
};
table.set("file_path", location_path.to_string_lossy())?;
table.set("start_line", location_start.line)?;
table.set("start_column", location_start.column)?;
table.set("end_line", location_end.line)?;
table.set("end_column", location_end.column)?;
table.set("version", crate::VERSION)?;
Ok(table)
}
#[expect(clippy::too_many_lines)]
fn add_input_to_globals<'a>(
&self,
identifier: &'a Identifier,
lua: &Lua,
scope: &Arc<Scope>,
) -> TranspileResult<(&'a str, Value)> {
let name = identifier.span.str();
let value = match scope.get_variable(name).as_deref() {
Some(VariableData::MacroParameter { macro_name, .. }) => Value::String(
lua.create_string(format!("$({macro_name})"))
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?,
),
Some(VariableData::Scoreboard { objective }) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("objective", objective.as_str())
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::ScoreboardValue { objective, target }) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set(
"objective",
lua.create_string(objective)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?,
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set(
"target",
lua.create_string(target)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?,
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::ScoreboardArray { objective, targets }) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("objective", objective.as_str())
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
let values = lua
.create_table_from(
targets
.iter()
.enumerate()
.map(|(i, target)| (i + 1, target.as_str())),
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("targets", values)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::BooleanStorage { storage_name, path }) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set(
"storage",
lua.create_string(storage_name)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?,
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set(
"path",
lua.create_string(path)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?,
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::BooleanStorageArray {
storage_name,
paths,
}) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("storage", storage_name.as_str())
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
let values = lua
.create_table_from(
paths
.iter()
.enumerate()
.map(|(i, path)| (i + 1, path.as_str())),
)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("paths", values)
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::Tag { tag_name }) => {
let table = lua
.create_table()
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
table
.set("name", tag_name.as_str())
.map_err(|err| LuaRuntimeError::from_lua_err(&err, self.span()))?;
Value::Table(table)
}
Some(VariableData::Function { .. } | VariableData::InternalFunction { .. }) => {
todo!("(internal) functions are not supported yet");
}
None => {
return Err(TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: identifier.span(),
}));
}
};
Ok((name, value))
}
fn handle_lua_result(
&self,
value: Value,

View File

@ -29,6 +29,9 @@ use strum::EnumIs;
#[cfg_attr(feature = "shulkerbox", doc(inline))]
pub use transpiler::Transpiler;
#[cfg(feature = "shulkerbox")]
pub mod internal_functions;
mod variables;
pub use variables::{Scope, VariableData};

View File

@ -101,7 +101,7 @@ impl Transpiler {
let scope = self
.scopes
.entry(program_identifier)
.or_default()
.or_insert_with(Scope::with_internal_functions)
.to_owned();
self.transpile_program_declarations(program, &scope, handler);
}
@ -129,7 +129,7 @@ impl Transpiler {
let scope = self
.scopes
.entry(identifier_span.source_file().identifier().to_owned())
.or_default()
.or_insert_with(Scope::with_internal_functions)
.to_owned();
self.get_or_transpile_function(&identifier_span, None, &scope, handler)?;
}
@ -441,7 +441,7 @@ impl Transpiler {
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_string().into())),
Some(val) => Ok(Parameter::Static(val.to_macro_string())),
None => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: expression.span(),
@ -831,60 +831,68 @@ impl Transpiler {
.arguments()
.as_ref()
.map(|l| l.elements().map(Deref::deref).collect::<Vec<_>>());
let (location, arguments) = self.get_or_transpile_function(
&func.identifier().span,
arguments.as_deref(),
scope,
handler,
)?;
let mut function_call = format!("function {location}");
match arguments {
TranspiledFunctionArguments::Static(arguments) => {
use std::fmt::Write;
let arguments_iter = arguments.iter().map(|(ident, v)| match v {
MacroString::String(s) => MacroString::String(format!(
r#"{macro_name}:"{escaped}""#,
macro_name = crate::util::identifier_to_macro(ident),
escaped = crate::util::escape_str(s)
)),
MacroString::MacroString(parts) => MacroString::MacroString(
std::iter::once(MacroStringPart::String(format!(
r#"{macro_name}:""#,
macro_name = crate::util::identifier_to_macro(ident)
)))
.chain(parts.clone().into_iter().map(|part| match part {
MacroStringPart::String(s) => {
MacroStringPart::String(crate::util::escape_str(&s).to_string())
}
macro_usage @ MacroStringPart::MacroUsage(_) => macro_usage,
}))
.chain(std::iter::once(MacroStringPart::String('"'.to_string())))
.collect(),
),
});
let arguments = super::util::join_macro_strings(arguments_iter);
if let Some(VariableData::InternalFunction { implementation }) =
scope.get_variable(func.identifier().span.str()).as_deref()
{
implementation(self, scope, func).inspect_err(|err| {
handler.receive(err.clone());
})
} else {
let (location, arguments) = self.get_or_transpile_function(
&func.identifier().span,
arguments.as_deref(),
scope,
handler,
)?;
let mut function_call = format!("function {location}");
match arguments {
TranspiledFunctionArguments::Static(arguments) => {
use std::fmt::Write;
let arguments_iter = arguments.iter().map(|(ident, v)| match v {
MacroString::String(s) => MacroString::String(format!(
r#"{macro_name}:"{escaped}""#,
macro_name = crate::util::identifier_to_macro(ident),
escaped = crate::util::escape_str(s)
)),
MacroString::MacroString(parts) => MacroString::MacroString(
std::iter::once(MacroStringPart::String(format!(
r#"{macro_name}:""#,
macro_name = crate::util::identifier_to_macro(ident)
)))
.chain(parts.clone().into_iter().map(|part| match part {
MacroStringPart::String(s) => {
MacroStringPart::String(crate::util::escape_str(&s).to_string())
}
macro_usage @ MacroStringPart::MacroUsage(_) => macro_usage,
}))
.chain(std::iter::once(MacroStringPart::String('"'.to_string())))
.collect(),
),
});
let arguments = super::util::join_macro_strings(arguments_iter);
let cmd = match arguments {
MacroString::String(arguments) => {
write!(function_call, " {{{arguments}}}").unwrap();
Command::Raw(function_call)
}
MacroString::MacroString(mut parts) => {
function_call.push_str(" {");
parts.insert(0, MacroStringPart::String(function_call));
parts.push(MacroStringPart::String('}'.to_string()));
Command::UsesMacro(MacroString::MacroString(parts).into())
}
};
let cmd = match arguments {
MacroString::String(arguments) => {
write!(function_call, " {{{arguments}}}").unwrap();
Command::Raw(function_call)
}
MacroString::MacroString(mut parts) => {
function_call.push_str(" {");
parts.insert(0, MacroStringPart::String(function_call));
parts.push(MacroStringPart::String('}'.to_string()));
Command::UsesMacro(MacroString::MacroString(parts).into())
}
};
Ok(vec![cmd])
Ok(vec![cmd])
}
TranspiledFunctionArguments::Dynamic(mut cmds) => {
function_call.push_str(" with storage shulkerscript:function_arguments");
cmds.push(Command::Raw(function_call));
Ok(cmds)
}
TranspiledFunctionArguments::None => Ok(vec![Command::Raw(function_call)]),
}
TranspiledFunctionArguments::Dynamic(mut cmds) => {
function_call.push_str(" with storage shulkerscript:function_arguments");
cmds.push(Command::Raw(function_call));
Ok(cmds)
}
TranspiledFunctionArguments::None => Ok(vec![Command::Raw(function_call)]),
}
}

View File

@ -1,7 +1,7 @@
#![expect(unused)]
use std::{
collections::HashMap,
collections::{HashMap, VecDeque},
fmt::Debug,
ops::Deref,
sync::{Arc, OnceLock, RwLock},
@ -20,15 +20,20 @@ use crate::{
syntax::syntax_tree::{
expression::{Expression, Primary},
statement::{
AssignmentDestination, ScoreVariableDeclaration, SingleVariableDeclaration,
VariableDeclaration,
ArrayVariableDeclaration, AssignmentDestination, ScoreVariableDeclaration,
SingleVariableDeclaration, TagVariableDeclaration, VariableDeclaration,
},
Annotation,
},
};
use super::{
error::{AssignmentError, IllegalAnnotationContent, MismatchedTypes},
error::{
AssignmentError, IllegalAnnotationContent, IllegalIndexing, IllegalIndexingReason,
MismatchedTypes,
},
expression::{ComptimeValue, DataLocation, ExpectedType, StorageType},
internal_functions::InternalFunction,
FunctionData, TranspileAnnotationValue, TranspileError, TranspileResult,
};
@ -90,6 +95,11 @@ pub enum VariableData {
/// The paths to the booleans.
paths: Vec<String>,
},
/// Compiler internal function.
InternalFunction {
/// The implementation
implementation: InternalFunction,
},
}
#[derive(Debug, Clone, Copy, EnumAsInner)]
@ -129,6 +139,19 @@ impl<'a> Scope<'a> {
Arc::new(Self::default())
}
/// Creates a new scope with internal functions.
#[cfg(feature = "shulkerbox")]
#[must_use]
pub fn with_internal_functions() -> Arc<Self> {
use super::internal_functions;
let scope = Self::new();
internal_functions::add_all_to_scope(&scope);
scope
}
/// Creates a new scope with a parent.
#[must_use]
pub fn with_parent(parent: &'a Arc<Self>) -> Arc<Self> {
@ -244,7 +267,18 @@ impl Transpiler {
scope,
handler,
),
_ => todo!("declarations other than single not supported yet: {declaration:?}"),
VariableDeclaration::Array(declaration) => self.transpile_array_variable_declaration(
declaration,
program_identifier,
scope,
handler,
),
VariableDeclaration::Tag(declaration) => self.transpile_tag_variable_declaration(
declaration,
program_identifier,
scope,
handler,
),
}
}
@ -311,23 +345,14 @@ impl Transpiler {
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let mut deobfuscate_annotations = declaration
.annotations()
.iter()
.filter(|a| a.has_identifier("deobfuscate"));
let deobfuscate_annotation = deobfuscate_annotations.next();
if let Some(duplicate) = deobfuscate_annotations.next() {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: duplicate.span(),
message: "Multiple deobfuscate annotations are not allowed.".to_string(),
});
handler.receive(error.clone());
return Err(error);
}
let name =
self.get_data_location_identifier(declaration, program_identifier, scope, handler)?;
let name = self.get_data_location_identifier(
declaration.int_keyword().keyword,
declaration.identifier(),
declaration.annotations(),
program_identifier,
scope,
handler,
)?;
let criteria = declaration
.criteria()
@ -350,6 +375,84 @@ impl Transpiler {
Ok(Vec::new())
}
fn transpile_array_variable_declaration(
&mut self,
declaration: &ArrayVariableDeclaration,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let variable_type = declaration.variable_type().keyword;
let (name, targets) =
self.get_array_data_location_pair(declaration, program_identifier, scope, handler)?;
match variable_type {
KeywordKind::Int => {
if !self.datapack.scoreboards().contains_key(&name) {
self.datapack
.register_scoreboard(&name, None::<&str>, None::<&str>);
}
scope.set_variable(
declaration.identifier().span.str(),
VariableData::ScoreboardArray {
objective: name,
targets,
},
);
}
KeywordKind::Bool => {
scope.set_variable(
declaration.identifier().span.str(),
VariableData::BooleanStorageArray {
storage_name: name,
paths: targets,
},
);
}
_ => unreachable!("no other variable types"),
}
declaration.assignment().as_ref().map_or_else(
|| Ok(Vec::new()),
|assignment| {
self.transpile_assignment(
TranspileAssignmentTarget::Identifier(declaration.identifier()),
assignment.expression(),
scope,
handler,
)
},
)
}
fn transpile_tag_variable_declaration(
&mut self,
declaration: &TagVariableDeclaration,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let name = self.get_data_location_identifier(
declaration.bool_keyword().keyword,
declaration.identifier(),
declaration.annotations(),
program_identifier,
scope,
handler,
)?;
scope.set_variable(
declaration.identifier().span.str(),
VariableData::Tag { tag_name: name },
);
// TODO: implement assignment when map literal is implemented
Ok(Vec::new())
}
#[expect(clippy::too_many_lines)]
pub(super) fn transpile_assignment(
&mut self,
destination: TranspileAssignmentTarget,
@ -388,27 +491,176 @@ impl Transpiler {
Some(ComptimeValue::MacroString(s)) => {
todo!("indexing scoreboard with macro string: {s}")
}
Some(_) => todo!("invalid indexing value"),
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: expression.span(),
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
});
handler.receive(err.clone());
return Err(err);
}
None => {
todo!("cannot assign to scoreboard without indexing")
// TODO: allow when map literals are implemented
let err = TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: "Cannot assign to a scoreboard without indexing".to_string(),
});
handler.receive(err.clone());
return Err(err);
}
},
VariableData::Function { .. } | VariableData::MacroParameter { .. } => {
VariableData::ScoreboardArray { objective, targets } => {
match indexing_value {
Some(ComptimeValue::Integer(index)) => {
if let Some(target) = usize::try_from(index)
.ok()
.and_then(|index| targets.get(index))
{
Ok(DataLocation::ScoreboardValue {
objective: objective.to_string(),
target: target.to_string(),
})
} else {
let index_span = match destination {
TranspileAssignmentTarget::Indexed(_, expr) => expr.span(),
TranspileAssignmentTarget::Identifier(_) => unreachable!(
"indexing value must be present (checked before)"
),
};
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: index_span,
reason: IllegalIndexingReason::IndexOutOfBounds {
index: usize::try_from(index).unwrap_or(usize::MAX),
length: targets.len(),
},
});
handler.receive(err.clone());
return Err(err);
}
}
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: expression.span(),
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
});
handler.receive(err.clone());
return Err(err);
}
None => {
// TODO: implement when array literals are implemented
let err = TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: "Cannot assign to an array without indexing".to_string(),
});
handler.receive(err.clone());
return Err(err);
}
}
}
VariableData::BooleanStorageArray {
storage_name,
paths,
} => {
match indexing_value {
Some(ComptimeValue::Integer(index)) => {
if let Some(path) = usize::try_from(index)
.ok()
.and_then(|index| paths.get(index))
{
Ok(DataLocation::Storage {
storage_name: storage_name.to_string(),
path: path.to_string(),
r#type: StorageType::Boolean,
})
} else {
let index_span = match destination {
TranspileAssignmentTarget::Indexed(_, expr) => expr.span(),
TranspileAssignmentTarget::Identifier(_) => unreachable!(
"indexing value must be present (checked before)"
),
};
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: index_span,
reason: IllegalIndexingReason::IndexOutOfBounds {
index: usize::try_from(index).unwrap_or(usize::MAX),
length: paths.len(),
},
});
handler.receive(err.clone());
return Err(err);
}
}
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: expression.span(),
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
});
handler.receive(err.clone());
return Err(err);
}
None => {
// TODO: implement when array literals are implemented
let err = TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: "Cannot assign to an array without indexing".to_string(),
});
handler.receive(err.clone());
return Err(err);
}
}
}
VariableData::Tag { tag_name } => match indexing_value {
Some(ComptimeValue::String(s)) => Ok(DataLocation::Tag {
tag_name: tag_name.to_string(),
entity: s,
}),
Some(ComptimeValue::MacroString(s)) => {
todo!("indexing tag with macro string: {s}")
}
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: expression.span(),
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
});
handler.receive(err.clone());
return Err(err);
}
None => {
// TODO: allow when map literals are implemented
let err = TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: "Cannot assign to a tag without indexing".to_string(),
});
handler.receive(err.clone());
return Err(err);
}
},
VariableData::Function { .. }
| VariableData::MacroParameter { .. }
| VariableData::InternalFunction { .. } => {
let err = TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: format!(
"Cannot assign to a {}.",
if matches!(target.as_ref(), VariableData::Function { .. }) {
"function"
} else {
"function argument"
match target.as_ref() {
VariableData::Function { .. } => "function",
VariableData::MacroParameter { .. } => "macro parameter",
VariableData::InternalFunction { .. } => "internal function",
_ => unreachable!(),
}
),
});
handler.receive(err.clone());
Err(err)
}
_ => todo!("implement other variable types"),
}?;
self.transpile_expression(expression, &data_location, scope, handler)
} else {
@ -423,13 +675,14 @@ impl Transpiler {
fn get_data_location_identifier(
&mut self,
declaration: &ScoreVariableDeclaration,
variable_type: KeywordKind,
identifier: &Identifier,
annotations: &VecDeque<Annotation>,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<String> {
let mut deobfuscate_annotations = declaration
.annotations()
let mut deobfuscate_annotations = annotations
.iter()
.filter(|a| a.has_identifier("deobfuscate"));
@ -453,6 +706,7 @@ impl Transpiler {
.comptime_eval(scope, handler)
.and_then(|val| val.to_string_no_macro())
{
// TODO: change invalid criteria if boolean
if !crate::util::is_valid_scoreboard_objective_name(&name_eval) {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
@ -471,9 +725,7 @@ impl Transpiler {
Err(error)
}
}
TranspileAnnotationValue::None => {
Ok(declaration.identifier().span.str().to_string())
}
TranspileAnnotationValue::None => Ok(identifier.span.str().to_string()),
TranspileAnnotationValue::Map(_) => {
let error =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
@ -488,7 +740,7 @@ impl Transpiler {
} else {
let hashed = md5::hash(program_identifier).to_hex_lowercase();
let name = "shu_values_".to_string() + &hashed;
let identifier_name = declaration.identifier().span.str();
let identifier_name = identifier.span.str();
let scope_ident = self.temp_counter;
self.temp_counter = self.temp_counter.wrapping_add(1);
let mut target = md5::hash(format!(
@ -634,6 +886,97 @@ impl Transpiler {
}
}
fn get_array_data_location_pair(
&mut self,
declaration: &ArrayVariableDeclaration,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<(String, Vec<String>)> {
let mut deobfuscate_annotations = declaration
.annotations()
.iter()
.filter(|a| a.has_identifier("deobfuscate"));
let variable_type = declaration.variable_type().keyword;
let deobfuscate_annotation = deobfuscate_annotations.next();
if let Some(duplicate) = deobfuscate_annotations.next() {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: duplicate.span(),
message: "Multiple deobfuscate annotations are not allowed.".to_string(),
});
handler.receive(error.clone());
return Err(error);
}
if let Some(deobfuscate_annotation) = deobfuscate_annotation {
let deobfuscate_annotation_value =
TranspileAnnotationValue::from(deobfuscate_annotation.assignment().value.clone());
match deobfuscate_annotation_value {
TranspileAnnotationValue::None => {
let ident_str = declaration.identifier().span.str();
let name = if matches!(variable_type, KeywordKind::Int) {
ident_str.to_string()
} else {
format!(
"{namespace}:{ident_str}",
namespace = self.main_namespace_name
)
};
let len = declaration.size().as_i64();
let targets = (0..len).map(|i| i.to_string()).collect();
Ok((name, targets))
}
TranspileAnnotationValue::Map(map) => {
todo!("allow map deobfuscate annotation for array variables")
}
TranspileAnnotationValue::Expression(_) => {
let error =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
message: "Deobfuscate annotation value must be a map or none."
.to_string(),
});
handler.receive(error.clone());
Err(error)
}
}
} else {
let hashed = md5::hash(program_identifier).to_hex_lowercase();
let name = if matches!(variable_type, KeywordKind::Int) {
"shu_values_"
} else {
"shulkerbox:values_"
}
.to_string()
+ &hashed;
let identifier_name = declaration.identifier().span.str();
let scope_ident = self.temp_counter;
self.temp_counter = self.temp_counter.wrapping_add(1);
let len = declaration.size().as_i64();
let targets = (0..len)
.map(|i| {
let mut target = md5::hash(format!(
"{scope_ident}\0{identifier_name}\0{i}\0{shadowed}",
shadowed = scope.get_variable_shadow_count(identifier_name)
))
.to_hex_lowercase();
if matches!(variable_type, KeywordKind::Int) {
target.split_off(16);
}
target
})
.collect();
Ok((name, targets))
}
}
/// Move data from location `from` to location `to`.
///
/// # Errors