Compare commits

...

3 Commits

Author SHA1 Message Date
Moritz Hölting e772c4b2c2 implement variable assignments 2025-03-07 17:36:25 +01:00
Moritz Hölting 94693cce6c fix generating same data location for shadowed variables 2025-03-07 17:35:31 +01:00
Moritz Hölting c72fbfd148 implement boolean storage variable 2025-03-07 16:47:48 +01:00
6 changed files with 341 additions and 117 deletions

View File

@ -38,7 +38,7 @@ path-absolutize = "3.1.1"
pathdiff = "0.2.3"
serde = { version = "1.0.217", features = ["derive"], optional = true }
# shulkerbox = { version = "0.1.0", default-features = false, optional = true }
shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "811d71508208f8415d881f7c4d73429f1a73f36a", default-features = false, optional = true }
shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "cf8c922704a38980def3c6267cee2eeba304394c", default-features = false, optional = true }
strsim = "0.11.1"
strum = { version = "0.27.0", features = ["derive"] }
thiserror = "2.0.11"

View File

@ -307,6 +307,10 @@ impl Semicolon {
SemicolonStatement::VariableDeclaration(decl) => {
decl.analyze_semantics(function_names, macro_names, handler)
}
SemicolonStatement::Assignment(_assignment) => {
// TODO: correctly analyze the semantics of the assignment
Ok(())
}
}
}
}

View File

@ -93,6 +93,14 @@ impl Statement {
})
})
}
SemicolonStatement::Assignment(_) => {
let err = Error::InvalidAnnotation(InvalidAnnotation {
annotation: annotation.assignment.identifier.span,
target: "assignments".to_string(),
});
Err(err)
}
SemicolonStatement::Expression(_) => {
let err = Error::InvalidAnnotation(InvalidAnnotation {
annotation: annotation.assignment.identifier.span,
@ -272,7 +280,7 @@ impl Semicolon {
/// Syntax Synopsis:
/// ``` ebnf
/// SemicolonStatement:
/// (Expression | VariableDeclaration)
/// (Expression | VariableDeclaration | Assignment)
/// ';'
/// ;
/// ```
@ -283,6 +291,8 @@ pub enum SemicolonStatement {
Expression(Expression),
/// A variable declaration.
VariableDeclaration(VariableDeclaration),
/// An assignment.
Assignment(Assignment),
}
impl SourceElement for SemicolonStatement {
@ -290,6 +300,7 @@ impl SourceElement for SemicolonStatement {
match self {
Self::Expression(expression) => expression.span(),
Self::VariableDeclaration(declaration) => declaration.span(),
Self::Assignment(assignment) => assignment.span(),
}
}
}
@ -669,6 +680,44 @@ impl TagVariableDeclaration {
}
}
/// Represents an assignment in the syntax tree.
///
/// Syntax Synopsis:
/// ```ebnf
/// Assignment:
/// Identifier '=' Expression
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)]
pub struct Assignment {
/// The identifier of the assignment.
#[get = "pub"]
identifier: Identifier,
/// The equals sign of the assignment.
#[get = "pub"]
equals: Punctuation,
/// The expression of the assignment.
#[get = "pub"]
expression: Expression,
}
impl SourceElement for Assignment {
fn span(&self) -> Span {
self.identifier
.span()
.join(&self.expression.span())
.expect("The span of the assignment is invalid.")
}
}
impl Assignment {
/// Dissolves the [`Assignment`] into its components.
#[must_use]
pub fn dissolve(self) -> (Identifier, Punctuation, Expression) {
(self.identifier, self.equals, self.expression)
}
}
impl<'a> Parser<'a> {
/// Parses a [`Block`].
///
@ -808,9 +857,27 @@ impl<'a> Parser<'a> {
self.parse_variable_declaration(handler)
.map(SemicolonStatement::VariableDeclaration)
}
_ => self
.parse_expression(handler)
.map(SemicolonStatement::Expression),
_ => {
// try to parse assignment
// TODO: improve
#[expect(clippy::option_if_let_else)]
if let Ok(assignment) = self.try_parse(|p| {
let identifier = p.parse_identifier(&VoidHandler)?;
let equals = p.parse_punctuation('=', true, &VoidHandler)?;
let expression = p.parse_expression(&VoidHandler)?;
Ok(SemicolonStatement::Assignment(Assignment {
identifier,
equals,
expression,
}))
}) {
Ok(assignment)
} else {
self.parse_expression(handler)
.map(SemicolonStatement::Expression)
}
}
}?;
let semicolon = self.parse_punctuation(';', true, handler)?;

View File

@ -36,6 +36,8 @@ pub enum TranspileError {
MismatchedTypes(#[from] MismatchedTypes),
#[error(transparent)]
FunctionArgumentsNotAllowed(#[from] FunctionArgumentsNotAllowed),
#[error(transparent)]
AssignmentError(#[from] AssignmentError),
}
/// The result of a transpilation operation.
@ -227,3 +229,25 @@ impl Display for FunctionArgumentsNotAllowed {
}
impl std::error::Error for FunctionArgumentsNotAllowed {}
/// An error that occurs when an expression can not evaluate to the wanted type.
#[expect(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignmentError {
pub identifier: Span,
pub message: String,
}
impl Display for AssignmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", Message::new(Severity::Error, &self.message))?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.identifier, Option::<u8>::None)
)
}
}
impl std::error::Error for AssignmentError {}

View File

@ -38,7 +38,9 @@ use super::{
/// A transpiler for `Shulkerscript`.
#[derive(Debug)]
pub struct Transpiler {
pub(super) main_namespace_name: String,
pub(super) datapack: shulkerbox::datapack::Datapack,
pub(super) setup_cmds: Vec<Command>,
/// Top-level [`Scope`] for each program identifier
scopes: BTreeMap<String, Arc<Scope<'static>>>,
/// Key: (program identifier, function name)
@ -51,8 +53,11 @@ impl Transpiler {
/// Creates a new transpiler.
#[must_use]
pub fn new(main_namespace_name: impl Into<String>, pack_format: u8) -> Self {
let main_namespace_name = main_namespace_name.into();
Self {
main_namespace_name: main_namespace_name.clone(),
datapack: shulkerbox::datapack::Datapack::new(main_namespace_name, pack_format),
setup_cmds: Vec::new(),
scopes: BTreeMap::new(),
functions: BTreeMap::new(),
aliases: HashMap::new(),
@ -119,6 +124,14 @@ impl Transpiler {
self.get_or_transpile_function(&identifier_span, None, &scope, handler)?;
}
if !self.setup_cmds.is_empty() {
let main_namespace = self.datapack.namespace_mut(&self.main_namespace_name);
let setup_fn = main_namespace.function_mut("shu/setup");
setup_fn.get_commands_mut().extend(self.setup_cmds.clone());
self.datapack
.add_load(format!("{}:shu/setup", self.main_namespace_name));
}
Ok(())
}
@ -218,8 +231,8 @@ impl Transpiler {
Declaration::Tag(tag) => {
let namespace = self
.datapack
.namespace_mut(&namespace.namespace_name().str_content());
let sb_tag = namespace.tag_mut(&tag.name().str_content(), tag.tag_type());
.namespace_mut(namespace.namespace_name().str_content());
let sb_tag = namespace.tag_mut(tag.name().str_content(), tag.tag_type());
if let Some(list) = &tag.entries().list {
for value in list.elements() {
@ -553,6 +566,12 @@ impl Transpiler {
SemicolonStatement::VariableDeclaration(decl) => {
self.transpile_variable_declaration(decl, program_identifier, scope, handler)
}
SemicolonStatement::Assignment(assignment) => self.transpile_assignment(
assignment.identifier(),
assignment.expression(),
scope,
handler,
),
},
}
}

View File

@ -8,12 +8,12 @@ use std::{
};
use chksum_md5 as md5;
use shulkerbox::prelude::Command;
use shulkerbox::prelude::{Command, Condition, Execute};
use strum::EnumIs;
use crate::{
base::{self, source_file::SourceElement as _, Handler},
lexical::token::KeywordKind,
lexical::token::{Identifier, KeywordKind},
syntax::syntax_tree::{
expression::{Expression, Primary},
statement::{SingleVariableDeclaration, VariableDeclaration},
@ -21,8 +21,9 @@ use crate::{
};
use super::{
error::IllegalAnnotationContent, expression::DataLocation, FunctionData,
TranspileAnnotationValue, TranspileError, TranspileResult, Transpiler,
error::{AssignmentError, IllegalAnnotationContent},
expression::DataLocation,
FunctionData, TranspileAnnotationValue, TranspileError, TranspileResult, Transpiler,
};
/// Stores the data required to access a variable.
@ -87,6 +88,8 @@ pub struct Scope<'a> {
parent: Option<&'a Arc<Self>>,
/// Variables stored in the scope.
variables: RwLock<HashMap<String, Arc<VariableData>>>,
/// How many times the variable has been shadowed in the current scope.
shadowed: RwLock<HashMap<String, usize>>,
}
impl<'a> Scope<'a> {
@ -117,12 +120,35 @@ impl<'a> Scope<'a> {
}
}
/// Gets the number of times a variable has been shadowed.
pub fn get_variable_shadow_count(&self, name: &str) -> usize {
let count = self
.shadowed
.read()
.unwrap()
.get(name)
.copied()
.unwrap_or(0);
self.parent.as_ref().map_or(count, |parent| {
count
+ parent.get_variable_shadow_count(name)
+ usize::from(parent.get_variable(name).is_some())
})
}
/// Sets a variable in the scope.
pub fn set_variable(&self, name: &str, var: VariableData) {
self.variables
let prev = self
.variables
.write()
.unwrap()
.insert(name.to_string(), Arc::new(var));
*self
.shadowed
.write()
.unwrap()
.entry(name.to_string())
.or_default() += 1;
}
/// Gets the variables stored in the current scope.
@ -161,6 +187,7 @@ impl<'a> Debug for Scope<'a> {
s.field("parent", &self.parent);
s.field("variables", &VariableWrapper(&self.variables));
s.field("shadowed", &self.shadowed);
s.finish()
}
}
@ -184,7 +211,6 @@ impl Transpiler {
}
}
#[expect(clippy::too_many_lines)]
fn transpile_single_variable_declaration(
&mut self,
single: &SingleVariableDeclaration,
@ -209,14 +235,125 @@ impl Transpiler {
handler.receive(error.clone());
return Err(error);
}
let (name, target) = if let Some(deobfuscate_annotation) = deobfuscate_annotation {
let (name, target) =
get_single_data_location_identifiers(single, 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(
single.identifier().span.str(),
VariableData::ScoreboardValue {
objective: name.clone(),
target,
},
);
}
KeywordKind::Bool => {
scope.set_variable(
single.identifier().span.str(),
VariableData::BooleanStorage {
storage_name: name,
path: target,
},
);
}
_ => todo!("implement other variable types"),
}
single.assignment().as_ref().map_or_else(
|| Ok(Vec::new()),
|assignment| {
self.transpile_assignment(
single.identifier(),
assignment.expression(),
scope,
handler,
)
},
)
}
pub(super) fn transpile_assignment(
&mut self,
identifier: &Identifier,
expression: &crate::syntax::syntax_tree::expression::Expression,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
if let Some(target) = scope.get_variable(identifier.span.str()) {
let data_location = match target.as_ref() {
VariableData::BooleanStorage { storage_name, path } => Ok(DataLocation::Storage {
storage_name: storage_name.to_owned(),
path: path.to_owned(),
r#type: super::expression::StorageType::Boolean,
}),
VariableData::ScoreboardValue { objective, target } => {
Ok(DataLocation::ScoreboardValue {
objective: objective.to_owned(),
target: target.to_owned(),
})
}
VariableData::Function { .. } | VariableData::FunctionArgument { .. } => {
Err(TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: format!(
"Cannot assign to a {}.",
if matches!(target.as_ref(), VariableData::Function { .. }) {
"function"
} else {
"function argument"
}
),
}))
}
_ => todo!("implement other variable types"),
}?;
self.transpile_expression(expression, &data_location, scope, handler)
} else {
Err(TranspileError::AssignmentError(AssignmentError {
identifier: identifier.span(),
message: "Variable does not exist.".to_string(),
}))
}
}
}
#[expect(clippy::too_many_lines)]
fn get_single_data_location_identifiers(
single: &SingleVariableDeclaration,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<(String, String)> {
let mut deobfuscate_annotations = single
.annotations()
.iter()
.filter(|a| a.has_identifier("deobfuscate"));
let variable_type = single.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());
if let TranspileAnnotationValue::Map(map) = deobfuscate_annotation_value {
if map.len() > 2 {
let error =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
message: "Deobfuscate annotation must have at most 2 key-value pairs."
.to_string(),
@ -250,14 +387,14 @@ impl Transpiler {
handler.receive(error.clone());
return Err(error);
}
(name_eval, target_eval)
Ok((name_eval, target_eval))
} else {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
message: "Deobfuscate annotation 'name' or 'target' could not have been evaluated at compile time.".to_string()
});
handler.receive(error.clone());
return Err(error);
Err(error)
}
} else {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
@ -265,18 +402,16 @@ impl Transpiler {
message: "Deobfuscate annotation 'name' and 'target' must be compile time expressions.".to_string()
});
handler.receive(error.clone());
return Err(error);
Err(error)
}
} else {
let error =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
message:
"Deobfuscate annotation must have both 'name' and 'target' keys."
message: "Deobfuscate annotation must have both 'name' and 'target' keys."
.to_string(),
});
handler.receive(error.clone());
return Err(error);
Err(error)
}
} else {
let error = TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
@ -284,69 +419,25 @@ impl Transpiler {
message: "Deobfuscate annotation must be a map.".to_string(),
});
handler.receive(error.clone());
return Err(error);
Err(error)
}
} else {
let name =
"shu_values_".to_string() + &md5::hash(program_identifier).to_hex_lowercase();
let target = md5::hash((Arc::as_ptr(scope) as usize).to_le_bytes())
.to_hex_lowercase()
.split_off(16);
let hashed = md5::hash(program_identifier).to_hex_lowercase();
let name = "shu_values_".to_string() + &hashed;
let identifier_name = single.identifier().span.str();
// TODO: generate same name each time (not dependent on pointer)
let mut target = md5::hash(format!(
"{scope}\0{identifier_name}\0{shadowed}",
scope = Arc::as_ptr(scope) as usize,
shadowed = scope.get_variable_shadow_count(identifier_name)
))
.to_hex_lowercase();
(name, target)
};
match variable_type {
KeywordKind::Int => {
if !self.datapack.scoreboards().contains_key(&name) {
self.datapack.register_scoreboard(&name, None, None);
if matches!(variable_type, KeywordKind::Int) {
target.split_off(16);
}
scope.set_variable(
single.identifier().span.str(),
VariableData::ScoreboardValue {
objective: name.clone(),
target,
},
);
}
_ => todo!("implement other variable types"),
}
single.assignment().as_ref().map_or_else(
|| Ok(Vec::new()),
|assignment| {
self.transpile_assignment(
single.identifier().span.str(),
assignment.expression(),
scope,
handler,
)
},
)
}
fn transpile_assignment(
&mut self,
name: &str,
expression: &crate::syntax::syntax_tree::expression::Expression,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let target = scope.get_variable(name).unwrap();
let data_location = match target.as_ref() {
VariableData::BooleanStorage { storage_name, path } => DataLocation::Storage {
storage_name: storage_name.to_owned(),
path: path.to_owned(),
r#type: super::expression::StorageType::Boolean,
},
VariableData::ScoreboardValue { objective, target } => DataLocation::ScoreboardValue {
objective: objective.to_owned(),
target: target.to_owned(),
},
_ => todo!("implement other variable types"),
};
self.transpile_expression(expression, &data_location, scope, handler)
Ok((name, target))
}
}
@ -392,4 +483,23 @@ mod tests {
panic!("Variable missing")
}
}
#[test]
fn test_shadowed_count() {
let scope = Scope::new();
scope.set_variable(
"test",
VariableData::Scoreboard {
objective: "test1".to_string(),
},
);
let child = Scope::with_parent(&scope);
child.set_variable(
"test",
VariableData::Scoreboard {
objective: "test2".to_string(),
},
);
assert_eq!(child.get_variable_shadow_count("test"), 1);
}
}