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"]
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" ]

View File

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

View File

@ -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.")
)
)
}
}

View File

@ -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<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 {
fn analyze_semantics(
&self,

View File

@ -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<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`].
///
/// # Errors

View File

@ -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::<Vec<_>>();
#[cfg(feature = "shulkerbox")]
let inner = inner.oxford_or();
#[cfg(not(feature = "shulkerbox"))]
let inner = std::borrow::Cow::<str>::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.")
)
)
}
}

View File

@ -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<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(
&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::<Vec<_>>();
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)) => {

View File

@ -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<MacroString> {
if self.contains_expression() {
let mut prepare_variables = BTreeMap::new();
let mut prepare_variables_reverse = HashMap::<DataLocation, String>::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)])
}