implement basic while loop
This commit is contained in:
parent
218f488e76
commit
7f276a4139
|
@ -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" ]
|
|
@ -148,6 +148,7 @@ Statement:
|
|||
| Grouping
|
||||
| DocComment
|
||||
| ExecuteBlock
|
||||
| WhileLoop
|
||||
| Semicolon
|
||||
;
|
||||
```
|
||||
|
@ -215,6 +216,14 @@ Semicolon:
|
|||
;
|
||||
```
|
||||
|
||||
## WhileLoop
|
||||
|
||||
```ebnf
|
||||
WhileLoop:
|
||||
'while' '(' Expression ')' Block
|
||||
;
|
||||
```
|
||||
|
||||
## FunctionVariableType
|
||||
|
||||
```ebnf
|
||||
|
|
|
@ -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.")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
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, Vec::new(), expression.span()),
|
||||
(
|
||||
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(),
|
||||
};
|
||||
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, Vec::new(), expression.span()),
|
||||
(
|
||||
data_location.clone(),
|
||||
Vec::new(),
|
||||
expression.span(),
|
||||
),
|
||||
);
|
||||
prepare_variables_reverse
|
||||
.insert(data_location, macro_name.clone());
|
||||
|
||||
macro_name
|
||||
};
|
||||
|
||||
Ok(vec![MacroStringPart::MacroUsage(macro_name)])
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue