implement basic while loop

This commit is contained in:
Moritz Hölting 2025-09-11 16:08:56 +02:00
parent 218f488e76
commit 7f276a4139
8 changed files with 345 additions and 39 deletions

View File

@ -19,7 +19,7 @@ default = ["fs_access", "lua", "shulkerbox", "zip"]
fs_access = ["shulkerbox?/fs_access"] fs_access = ["shulkerbox?/fs_access"]
lua = ["dep:mlua"] lua = ["dep:mlua"]
serde = ["dep:serde", "dep:serde_json", "shulkerbox?/serde"] 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"] zip = ["shulkerbox?/zip"]
[dependencies] [dependencies]
@ -36,7 +36,7 @@ pathdiff = "0.2.3"
serde = { version = "1.0.217", features = ["derive"], optional = true } serde = { version = "1.0.217", features = ["derive"], optional = true }
serde_json = { version = "1.0.138", optional = true } serde_json = { version = "1.0.138", optional = true }
# shulkerbox = { version = "0.1.0", default-features = false, 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" strsim = "0.11.1"
strum = { version = "0.27.0", features = ["derive"] } strum = { version = "0.27.0", features = ["derive"] }
thiserror = "2.0.11" thiserror = "2.0.11"
@ -57,3 +57,6 @@ required-features = ["shulkerbox"]
[[test]] [[test]]
name = "transpiling" name = "transpiling"
required-features = ["shulkerbox"] required-features = ["shulkerbox"]
[package.metadata.cargo-feature-combinations]
exclude_features = [ "default" ]

View File

@ -148,6 +148,7 @@ Statement:
| Grouping | Grouping
| DocComment | DocComment
| ExecuteBlock | ExecuteBlock
| WhileLoop
| Semicolon | Semicolon
; ;
``` ```
@ -215,6 +216,14 @@ Semicolon:
; ;
``` ```
## WhileLoop
```ebnf
WhileLoop:
'while' '(' Expression ')' Block
;
```
## FunctionVariableType ## FunctionVariableType
```ebnf ```ebnf

View File

@ -35,6 +35,8 @@ pub enum Error {
UnknownIdentifier(#[from] UnknownIdentifier), UnknownIdentifier(#[from] UnknownIdentifier),
#[error(transparent)] #[error(transparent)]
AssignmentError(#[from] AssignmentError), AssignmentError(#[from] AssignmentError),
#[error(transparent)]
NeverLoops(#[from] NeverLoops),
#[error("Lua is disabled, but a Lua function was used.")] #[error("Lua is disabled, but a Lua function was used.")]
LuaDisabled, LuaDisabled,
#[error("Other: {0}")] #[error("Other: {0}")]
@ -228,3 +230,27 @@ impl Display for InvalidFunctionArguments {
} }
impl std::error::Error 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.")
)
)
}
}

View File

@ -3,8 +3,13 @@
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
use crate::{ use crate::{
base::{self, source_file::SourceElement as _, Handler}, base::{
self,
source_file::{SourceElement as _, Span},
Handler,
},
lexical::token::KeywordKind, lexical::token::KeywordKind,
semantic::error::NeverLoops,
syntax::syntax_tree::{ syntax::syntax_tree::{
declaration::{Declaration, Function, FunctionVariableType, ImportItems}, declaration::{Declaration, Function, FunctionVariableType, ImportItems},
expression::{ expression::{
@ -18,7 +23,7 @@ use crate::{
ExecuteBlockTail, ExecuteBlockTail,
}, },
Assignment, AssignmentDestination, Block, Grouping, Semicolon, SemicolonStatement, Assignment, AssignmentDestination, Block, Grouping, Semicolon, SemicolonStatement,
Statement, VariableDeclaration, Statement, VariableDeclaration, WhileLoop,
}, },
AnyStringLiteral, AnyStringLiteral,
}, },
@ -254,6 +259,7 @@ impl Statement {
let child_scope = SemanticScope::with_parent(scope); let child_scope = SemanticScope::with_parent(scope);
group.analyze_semantics(&child_scope, handler) group.analyze_semantics(&child_scope, handler)
} }
Self::WhileLoop(while_loop) => while_loop.analyze_semantics(scope, handler),
Self::Semicolon(sem) => sem.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<Span> {
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<Span> {
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<base::Error>,
) -> 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 { impl Semicolon {
fn analyze_semantics( fn analyze_semantics(
&self, &self,

View File

@ -23,7 +23,7 @@ use crate::{
}, },
syntax::{ syntax::{
error::{Error, InvalidAnnotation, ParseResult, SyntaxKind, UnexpectedSyntax}, error::{Error, InvalidAnnotation, ParseResult, SyntaxKind, UnexpectedSyntax},
parser::{Parser, Reading}, parser::{DelimitedTree, Parser, Reading},
}, },
}; };
@ -43,6 +43,7 @@ use super::{expression::Expression, Annotation, AnyStringLiteral};
/// | Grouping /// | Grouping
/// | DocComment /// | DocComment
/// | ExecuteBlock /// | ExecuteBlock
/// | WhileLoop
/// | Semicolon /// | Semicolon
/// ; /// ;
/// ``` /// ```
@ -55,6 +56,7 @@ pub enum Statement {
ExecuteBlock(ExecuteBlock), ExecuteBlock(ExecuteBlock),
Grouping(Grouping), Grouping(Grouping),
DocComment(DocComment), DocComment(DocComment),
WhileLoop(WhileLoop),
Semicolon(Semicolon), Semicolon(Semicolon),
} }
@ -66,6 +68,7 @@ impl SourceElement for Statement {
Self::ExecuteBlock(execute_block) => execute_block.span(), Self::ExecuteBlock(execute_block) => execute_block.span(),
Self::Grouping(grouping) => grouping.span(), Self::Grouping(grouping) => grouping.span(),
Self::DocComment(doc_comment) => doc_comment.span(), Self::DocComment(doc_comment) => doc_comment.span(),
Self::WhileLoop(while_loop) => while_loop.span(),
Self::Semicolon(semi) => semi.span(), Self::Semicolon(semi) => semi.span(),
} }
} }
@ -183,7 +186,7 @@ pub struct Grouping {
/// The `group` keyword. /// The `group` keyword.
#[get = "pub"] #[get = "pub"]
group_keyword: Keyword, group_keyword: Keyword,
/// The block of the conditional. /// The block of the grouping.
#[get = "pub"] #[get = "pub"]
block: Block, 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. /// Represents a statement that ends with a semicolon in the syntax tree.
/// ///
/// Syntax Synopsis: /// 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 // semicolon statement
_ => self.parse_semicolon(handler).map(Statement::Semicolon), _ => 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<base::Error>,
) -> ParseResult<WhileLoop> {
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`]. /// Parses a [`Semicolon`].
/// ///
/// # Errors /// # Errors

View File

@ -3,6 +3,8 @@
use std::fmt::Display; use std::fmt::Display;
use getset::Getters; use getset::Getters;
#[cfg(feature = "shulkerbox")]
use oxford_join::OxfordJoin as _; use oxford_join::OxfordJoin as _;
use crate::{ use crate::{
@ -47,6 +49,8 @@ pub enum TranspileError {
InvalidArgument(#[from] InvalidArgument), InvalidArgument(#[from] InvalidArgument),
#[error(transparent)] #[error(transparent)]
NotComptime(#[from] NotComptime), NotComptime(#[from] NotComptime),
#[error(transparent)]
InfiniteLoop(#[from] InfiniteLoop),
} }
/// The result of a transpilation operation. /// The result of a transpilation operation.
@ -263,7 +267,11 @@ impl Display for UnknownIdentifier {
.iter() .iter()
.map(|s| format!("`{s}`")) .map(|s| format!("`{s}`"))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
#[cfg(feature = "shulkerbox")]
let inner = inner.oxford_or(); let inner = inner.oxford_or();
#[cfg(not(feature = "shulkerbox"))]
let inner = std::borrow::Cow::<str>::Owned(inner.join(", "));
Some(message + &inner + "?") Some(message + &inner + "?")
}; };
@ -436,3 +444,29 @@ impl Display for NotComptime {
} }
impl std::error::Error 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.")
)
)
}
}

View File

@ -7,7 +7,7 @@ use std::{
}; };
use itertools::Itertools; use itertools::Itertools;
use shulkerbox::datapack::{self, Command, Datapack, Execute, Group}; use shulkerbox::datapack::{self, Command, Datapack, Execute, Group, While as WhileCmd};
use crate::{ use crate::{
base::{ base::{
@ -22,13 +22,13 @@ use crate::{
program::{Namespace, ProgramFile}, program::{Namespace, ProgramFile},
statement::{ statement::{
execute_block::{Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockTail}, execute_block::{Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockTail},
ReturnStatement, SemicolonStatement, Statement, Block, ReturnStatement, SemicolonStatement, Statement,
}, },
AnnotationAssignment, AnnotationAssignment,
}, },
transpile::{ transpile::{
conversions::ShulkerboxMacroStringMap, conversions::ShulkerboxMacroStringMap,
error::IllegalAnnotationContent, error::{IllegalAnnotationContent, InfiniteLoop},
expression::DataLocation, expression::DataLocation,
util::{MacroString, MacroStringPart}, util::{MacroString, MacroStringPart},
variables::FunctionVariableDataType, variables::FunctionVariableDataType,
@ -365,6 +365,33 @@ impl Transpiler {
Ok(()) Ok(())
} }
pub(super) fn transpile_block(
&mut self,
block: &Block,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
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::<Vec<_>>();
if !errors.is_empty() {
return Err(errors.remove(0));
}
Ok(commands)
}
pub(super) fn transpile_statement( pub(super) fn transpile_statement(
&mut self, &mut self,
statement: &Statement, statement: &Statement,
@ -393,33 +420,46 @@ impl Transpiler {
Ok(vec![Command::Comment(content.to_string())]) Ok(vec![Command::Comment(content.to_string())])
} }
Statement::Grouping(group) => { Statement::Grouping(group) => {
let child_scope = Scope::with_parent(scope.clone()); let commands =
let statements = group.block().statements(); self.transpile_block(group.block(), program_identifier, scope, handler)?;
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::<Vec<_>>();
if !errors.is_empty() {
return Err(errors.remove(0));
}
if commands.is_empty() { if commands.is_empty() {
Ok(Vec::new()) Ok(Vec::new())
} else { } else {
Ok(vec![Command::Group(Group::new(commands))]) 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() { Statement::Semicolon(semi) => match semi.statement() {
SemicolonStatement::Expression(expr) => match expr { SemicolonStatement::Expression(expr) => match expr {
Expression::Primary(Primary::FunctionCall(func)) => { Expression::Primary(Primary::FunctionCall(func)) => {

View File

@ -1,7 +1,10 @@
//! Utility methods for transpiling //! Utility methods for transpiling
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
use std::{collections::BTreeMap, sync::Arc}; use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
use shulkerbox::prelude::Command; use shulkerbox::prelude::Command;
@ -347,6 +350,7 @@ impl TemplateStringLiteral {
) -> TranspileResult<MacroString> { ) -> TranspileResult<MacroString> {
if self.contains_expression() { if self.contains_expression() {
let mut prepare_variables = BTreeMap::new(); let mut prepare_variables = BTreeMap::new();
let mut prepare_variables_reverse = HashMap::<DataLocation, String>::new();
let parts = self let parts = self
.parts() .parts()
@ -400,10 +404,25 @@ impl TemplateStringLiteral {
path: path.to_owned(), path: path.to_owned(),
r#type: StorageType::Boolean, r#type: StorageType::Boolean,
}; };
prepare_variables.insert(
macro_name.clone(), let macro_name = if let Some(prev_macro_name) =
(data_location, Vec::new(), expression.span()), 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)]) Ok(vec![MacroStringPart::MacroUsage(macro_name)])
} }
@ -421,10 +440,24 @@ impl TemplateStringLiteral {
objective: objective.to_owned(), objective: objective.to_owned(),
target: target.to_owned(), target: target.to_owned(),
}; };
prepare_variables.insert( let macro_name = if let Some(prev_macro_name) =
macro_name.clone(), prepare_variables_reverse.get(&data_location)
(data_location, Vec::new(), expression.span()), {
); 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)]) Ok(vec![MacroStringPart::MacroUsage(macro_name)])
} }