From 7f276a41392e7e7c7ef93466fb8e977782be8678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:08:56 +0200 Subject: [PATCH] implement basic while loop --- Cargo.toml | 7 +- grammar.md | 9 +++ src/semantic/error.rs | 26 +++++++ src/semantic/mod.rs | 67 +++++++++++++++++- src/syntax/syntax_tree/statement.rs | 102 +++++++++++++++++++++++++++- src/transpile/error.rs | 34 ++++++++++ src/transpile/transpiler.rs | 88 +++++++++++++++++------- src/transpile/util.rs | 51 +++++++++++--- 8 files changed, 345 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 342c10d..ab871f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ default = ["fs_access", "lua", "shulkerbox", "zip"] fs_access = ["shulkerbox?/fs_access"] lua = ["dep:mlua"] serde = ["dep:serde", "dep:serde_json", "shulkerbox?/serde"] -shulkerbox = ["dep:shulkerbox", "dep:chksum-md5", "dep:serde_json"] +shulkerbox = ["dep:shulkerbox", "dep:chksum-md5", "dep:serde_json", "dep:oxford_join"] zip = ["shulkerbox?/zip"] [dependencies] @@ -36,7 +36,7 @@ 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 = "d4689c696a35328c041bcbbfd203abd5818c46d3", default-features = false, optional = true } +shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "e1bc953b7a1692c65f1ed2c43fa3b0c607df8090", default-features = false, optional = true } strsim = "0.11.1" strum = { version = "0.27.0", features = ["derive"] } thiserror = "2.0.11" @@ -57,3 +57,6 @@ required-features = ["shulkerbox"] [[test]] name = "transpiling" required-features = ["shulkerbox"] + +[package.metadata.cargo-feature-combinations] +exclude_features = [ "default" ] \ No newline at end of file diff --git a/grammar.md b/grammar.md index 5d50e56..255ff26 100644 --- a/grammar.md +++ b/grammar.md @@ -148,6 +148,7 @@ Statement: | Grouping | DocComment | ExecuteBlock + | WhileLoop | Semicolon ; ``` @@ -215,6 +216,14 @@ Semicolon: ; ``` +## WhileLoop + +```ebnf +WhileLoop: + 'while' '(' Expression ')' Block + ; +``` + ## FunctionVariableType ```ebnf diff --git a/src/semantic/error.rs b/src/semantic/error.rs index 962f916..ebf4bcd 100644 --- a/src/semantic/error.rs +++ b/src/semantic/error.rs @@ -35,6 +35,8 @@ pub enum Error { UnknownIdentifier(#[from] UnknownIdentifier), #[error(transparent)] AssignmentError(#[from] AssignmentError), + #[error(transparent)] + NeverLoops(#[from] NeverLoops), #[error("Lua is disabled, but a Lua function was used.")] LuaDisabled, #[error("Other: {0}")] @@ -228,3 +230,27 @@ impl Display for InvalidFunctionArguments { } impl std::error::Error for InvalidFunctionArguments {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)] +pub struct NeverLoops { + pub reason: Span, +} + +impl Display for NeverLoops { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + Message::new(Severity::Error, "Loop never actually loops.") + )?; + + write!( + f, + "\n{}", + SourceCodeDisplay::new( + &self.reason, + Some("This statement causes the loop to always terminate.") + ) + ) + } +} diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index e5fc7c1..fd12db9 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -3,8 +3,13 @@ #![allow(clippy::missing_errors_doc)] use crate::{ - base::{self, source_file::SourceElement as _, Handler}, + base::{ + self, + source_file::{SourceElement as _, Span}, + Handler, + }, lexical::token::KeywordKind, + semantic::error::NeverLoops, syntax::syntax_tree::{ declaration::{Declaration, Function, FunctionVariableType, ImportItems}, expression::{ @@ -18,7 +23,7 @@ use crate::{ ExecuteBlockTail, }, Assignment, AssignmentDestination, Block, Grouping, Semicolon, SemicolonStatement, - Statement, VariableDeclaration, + Statement, VariableDeclaration, WhileLoop, }, AnyStringLiteral, }, @@ -254,6 +259,7 @@ impl Statement { let child_scope = SemanticScope::with_parent(scope); group.analyze_semantics(&child_scope, handler) } + Self::WhileLoop(while_loop) => while_loop.analyze_semantics(scope, handler), Self::Semicolon(sem) => sem.analyze_semantics(scope, handler), } } @@ -367,6 +373,63 @@ impl Grouping { } } +fn block_contains_unconditional_return(block: &Block) -> Option { + block + .statements + .iter() + .find_map(|statement| match statement { + Statement::Semicolon(semicolon) => { + let s = semicolon.statement(); + if s.is_return() { + Some(s.span()) + } else { + None + } + } + Statement::Grouping(group) => block_contains_unconditional_return(group.block()), + Statement::ExecuteBlock(ex) => execute_block_contains_unconditional_return(ex), + _ => None, + }) +} + +fn execute_block_contains_unconditional_return(ex: &ExecuteBlock) -> Option { + match ex { + ExecuteBlock::HeadTail(head, tail) => { + if head.is_conditional() { + None + } else { + match tail { + ExecuteBlockTail::Block(inner_block) => { + block_contains_unconditional_return(inner_block) + } + ExecuteBlockTail::ExecuteBlock(_, inner_ex) => { + execute_block_contains_unconditional_return(inner_ex) + } + } + } + } + ExecuteBlock::IfElse(..) => None, + } +} + +impl WhileLoop { + fn analyze_semantics( + &self, + scope: &SemanticScope, + handler: &impl Handler, + ) -> Result<(), error::Error> { + self.condition().analyze_semantics(scope, handler)?; + + if let Some(reason) = block_contains_unconditional_return(self.block()) { + let err = error::Error::NeverLoops(NeverLoops { reason }); + handler.receive(err.clone()); + return Err(err); + } + + self.block().analyze_semantics(scope, handler) + } +} + impl Semicolon { fn analyze_semantics( &self, diff --git a/src/syntax/syntax_tree/statement.rs b/src/syntax/syntax_tree/statement.rs index 1e1dbd0..19bfdc7 100644 --- a/src/syntax/syntax_tree/statement.rs +++ b/src/syntax/syntax_tree/statement.rs @@ -23,7 +23,7 @@ use crate::{ }, syntax::{ error::{Error, InvalidAnnotation, ParseResult, SyntaxKind, UnexpectedSyntax}, - parser::{Parser, Reading}, + parser::{DelimitedTree, Parser, Reading}, }, }; @@ -43,6 +43,7 @@ use super::{expression::Expression, Annotation, AnyStringLiteral}; /// | Grouping /// | DocComment /// | ExecuteBlock +/// | WhileLoop /// | Semicolon /// ; /// ``` @@ -55,6 +56,7 @@ pub enum Statement { ExecuteBlock(ExecuteBlock), Grouping(Grouping), DocComment(DocComment), + WhileLoop(WhileLoop), Semicolon(Semicolon), } @@ -66,6 +68,7 @@ impl SourceElement for Statement { Self::ExecuteBlock(execute_block) => execute_block.span(), Self::Grouping(grouping) => grouping.span(), Self::DocComment(doc_comment) => doc_comment.span(), + Self::WhileLoop(while_loop) => while_loop.span(), Self::Semicolon(semi) => semi.span(), } } @@ -183,7 +186,7 @@ pub struct Grouping { /// The `group` keyword. #[get = "pub"] group_keyword: Keyword, - /// The block of the conditional. + /// The block of the grouping. #[get = "pub"] block: Block, } @@ -205,6 +208,58 @@ impl SourceElement for Grouping { } } +/// Represents a while loop in the syntax tree. +/// +/// Syntax Synopsis: +/// +/// ```ebnf +/// WhileLoop: +/// 'while' '(' Expression ')' Block +/// ; +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)] +pub struct WhileLoop { + /// The `while` keyword. + #[get = "pub"] + while_keyword: Keyword, + /// The opening parenthesis + #[get = "pub"] + open_paren: Punctuation, + /// The condition expression + #[get = "pub"] + condition: Expression, + /// The closing parenthesis + #[get = "pub"] + close_paren: Punctuation, + /// The block of the loop. + #[get = "pub"] + block: Block, +} + +impl SourceElement for WhileLoop { + fn span(&self) -> Span { + self.while_keyword + .span + .join(&self.block.span()) + .expect("spans in same file") + } +} + +impl WhileLoop { + /// Dissolves the [`WhileLoop`] into its components. + #[must_use] + pub fn dissolve(self) -> (Keyword, Punctuation, Expression, Punctuation, Block) { + ( + self.while_keyword, + self.open_paren, + self.condition, + self.close_paren, + self.block, + ) + } +} + /// Represents a statement that ends with a semicolon in the syntax tree. /// /// Syntax Synopsis: @@ -920,11 +975,54 @@ impl Parser<'_> { })) } + // while loop + Reading::Atomic(Token::Keyword(while_keyword)) + if while_keyword.keyword == KeywordKind::While => + { + self.parse_while_loop(handler).map(Statement::WhileLoop) + } + // semicolon statement _ => self.parse_semicolon(handler).map(Statement::Semicolon), } } + /// Parses a [`WhileLoop`]. + /// + /// # Errors + /// - if the parser is not at a while loop + #[tracing::instrument(level = "trace", skip_all)] + pub fn parse_while_loop( + &mut self, + handler: &impl Handler, + ) -> ParseResult { + let while_keyword = self.parse_keyword(KeywordKind::While, handler)?; + + self.stop_at_significant(); + + let DelimitedTree { + open: open_paren, + tree, + close: close_paren, + } = self.step_into( + Delimiter::Parenthesis, + |p| p.parse_expression(handler), + handler, + )?; + + let condition = tree?; + + let block = self.parse_block(handler)?; + + Ok(WhileLoop { + while_keyword, + open_paren, + condition, + close_paren, + block, + }) + } + /// Parses a [`Semicolon`]. /// /// # Errors diff --git a/src/transpile/error.rs b/src/transpile/error.rs index b350084..b3ade30 100644 --- a/src/transpile/error.rs +++ b/src/transpile/error.rs @@ -3,6 +3,8 @@ use std::fmt::Display; use getset::Getters; + +#[cfg(feature = "shulkerbox")] use oxford_join::OxfordJoin as _; use crate::{ @@ -47,6 +49,8 @@ pub enum TranspileError { InvalidArgument(#[from] InvalidArgument), #[error(transparent)] NotComptime(#[from] NotComptime), + #[error(transparent)] + InfiniteLoop(#[from] InfiniteLoop), } /// The result of a transpilation operation. @@ -263,7 +267,11 @@ impl Display for UnknownIdentifier { .iter() .map(|s| format!("`{s}`")) .collect::>(); + + #[cfg(feature = "shulkerbox")] let inner = inner.oxford_or(); + #[cfg(not(feature = "shulkerbox"))] + let inner = std::borrow::Cow::::Owned(inner.join(", ")); Some(message + &inner + "?") }; @@ -436,3 +444,29 @@ impl Display for NotComptime { } impl std::error::Error for NotComptime {} + +/// An error that occurs when a loop never terminates. +#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)] +pub struct InfiniteLoop { + /// The condition making it not terminate. + pub span: Span, +} + +impl Display for InfiniteLoop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + Message::new(Severity::Error, "Loop never terminates.") + )?; + + write!( + f, + "\n{}", + SourceCodeDisplay::new( + &self.span, + Some("You may want to use a separate function with the `#[tick]` annotation.") + ) + ) + } +} diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index af1f51a..c057de9 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -7,7 +7,7 @@ use std::{ }; use itertools::Itertools; -use shulkerbox::datapack::{self, Command, Datapack, Execute, Group}; +use shulkerbox::datapack::{self, Command, Datapack, Execute, Group, While as WhileCmd}; use crate::{ base::{ @@ -22,13 +22,13 @@ use crate::{ program::{Namespace, ProgramFile}, statement::{ execute_block::{Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockTail}, - ReturnStatement, SemicolonStatement, Statement, + Block, ReturnStatement, SemicolonStatement, Statement, }, AnnotationAssignment, }, transpile::{ conversions::ShulkerboxMacroStringMap, - error::IllegalAnnotationContent, + error::{IllegalAnnotationContent, InfiniteLoop}, expression::DataLocation, util::{MacroString, MacroStringPart}, variables::FunctionVariableDataType, @@ -365,6 +365,33 @@ impl Transpiler { Ok(()) } + pub(super) fn transpile_block( + &mut self, + block: &Block, + program_identifier: &str, + scope: &Arc, + handler: &impl Handler, + ) -> TranspileResult> { + let child_scope = Scope::with_parent(scope.clone()); + let statements = block.statements(); + let mut errors = Vec::new(); + let commands = statements + .iter() + .flat_map(|statement| { + self.transpile_statement(statement, program_identifier, &child_scope, handler) + .unwrap_or_else(|err| { + errors.push(err); + Vec::new() + }) + }) + .collect::>(); + if !errors.is_empty() { + return Err(errors.remove(0)); + } + + Ok(commands) + } + pub(super) fn transpile_statement( &mut self, statement: &Statement, @@ -393,33 +420,46 @@ impl Transpiler { Ok(vec![Command::Comment(content.to_string())]) } Statement::Grouping(group) => { - let child_scope = Scope::with_parent(scope.clone()); - let statements = group.block().statements(); - let mut errors = Vec::new(); - let commands = statements - .iter() - .flat_map(|statement| { - self.transpile_statement( - statement, - program_identifier, - &child_scope, - handler, - ) - .unwrap_or_else(|err| { - errors.push(err); - Vec::new() - }) - }) - .collect::>(); - if !errors.is_empty() { - return Err(errors.remove(0)); - } + let commands = + self.transpile_block(group.block(), program_identifier, scope, handler)?; if commands.is_empty() { Ok(Vec::new()) } else { Ok(vec![Command::Group(Group::new(commands))]) } } + Statement::WhileLoop(while_loop) => { + let (mut condition_commands, prepare_variables, condition) = + self.transpile_expression_as_condition(while_loop.condition(), scope, handler)?; + + match condition { + ExtendedCondition::Comptime(false) => Ok(Vec::new()), + ExtendedCondition::Comptime(true) => { + let err = TranspileError::InfiniteLoop(InfiniteLoop { + span: while_loop.condition().span(), + }); + handler.receive(Box::new(err.clone())); + Err(err) + } + ExtendedCondition::Runtime(condition) => { + let loop_commands = self.transpile_block( + while_loop.block(), + program_identifier, + scope, + handler, + )?; + + condition_commands + .push(Command::While(WhileCmd::new(condition, loop_commands))); + + self.transpile_commands_with_variable_macros( + condition_commands, + prepare_variables, + handler, + ) + } + } + } Statement::Semicolon(semi) => match semi.statement() { SemicolonStatement::Expression(expr) => match expr { Expression::Primary(Primary::FunctionCall(func)) => { diff --git a/src/transpile/util.rs b/src/transpile/util.rs index 378715c..7b30638 100644 --- a/src/transpile/util.rs +++ b/src/transpile/util.rs @@ -1,7 +1,10 @@ //! Utility methods for transpiling #[cfg(feature = "shulkerbox")] -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; #[cfg(feature = "shulkerbox")] use shulkerbox::prelude::Command; @@ -347,6 +350,7 @@ impl TemplateStringLiteral { ) -> TranspileResult { if self.contains_expression() { let mut prepare_variables = BTreeMap::new(); + let mut prepare_variables_reverse = HashMap::::new(); let parts = self .parts() @@ -400,10 +404,25 @@ impl TemplateStringLiteral { path: path.to_owned(), r#type: StorageType::Boolean, }; - prepare_variables.insert( - macro_name.clone(), - (data_location, Vec::new(), expression.span()), - ); + + let macro_name = if let Some(prev_macro_name) = + prepare_variables_reverse.get(&data_location) + { + prev_macro_name.to_string() + } else { + prepare_variables.insert( + macro_name.clone(), + ( + data_location.clone(), + Vec::new(), + expression.span(), + ), + ); + prepare_variables_reverse + .insert(data_location, macro_name.clone()); + + macro_name + }; Ok(vec![MacroStringPart::MacroUsage(macro_name)]) } @@ -421,10 +440,24 @@ impl TemplateStringLiteral { objective: objective.to_owned(), target: target.to_owned(), }; - prepare_variables.insert( - macro_name.clone(), - (data_location, Vec::new(), expression.span()), - ); + let macro_name = if let Some(prev_macro_name) = + prepare_variables_reverse.get(&data_location) + { + prev_macro_name.to_string() + } else { + prepare_variables.insert( + macro_name.clone(), + ( + data_location.clone(), + Vec::new(), + expression.span(), + ), + ); + prepare_variables_reverse + .insert(data_location, macro_name.clone()); + + macro_name + }; Ok(vec![MacroStringPart::MacroUsage(macro_name)]) }