Merge branch 'feature/macro-functions' into develop

This commit is contained in:
Moritz Hölting 2025-02-17 13:58:58 +01:00
commit 96fe865ac1
24 changed files with 1811 additions and 203 deletions

View File

@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Macro strings
- Function parameters/arguments
### Changed ### Changed
- Option to deduplicate source files during serialization when using `SerdeWrapper` - Option to deduplicate source files during serialization when using `SerdeWrapper`

View File

@ -37,7 +37,8 @@ mlua = { version = "0.10.2", features = ["lua54", "vendored"], optional = true }
path-absolutize = "3.1.1" path-absolutize = "3.1.1"
pathdiff = "0.2.3" pathdiff = "0.2.3"
serde = { version = "1.0.217", features = ["derive"], optional = true } serde = { version = "1.0.217", features = ["derive"], 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 = "76d58c0766518fe5ab2635de60ba40972565a3e0", 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"

View File

@ -12,6 +12,21 @@ Program: Namespace Declaration*;
Namespace: 'namespace' StringLiteral; Namespace: 'namespace' StringLiteral;
``` ```
### StringLiteral
```ebnf
StringLiteral: '"' TEXT '"';
```
### MacroStringLiteral
```ebnf
MacroStringLiteral: '`' ( TEXT | '$(' [a-zA-Z0-9_]+ ')' )* '`';
```
### AnyStringLiteral
```ebnf
AnyStringLiteral: StringLiteral | MacroStringLiteral;
```
### Declaration ### Declaration
```ebnf ```ebnf
Declaration: FunctionDeclaration | Import | TagDeclaration; Declaration: FunctionDeclaration | Import | TagDeclaration;
@ -87,7 +102,7 @@ Condition:
PrimaryCondition: PrimaryCondition:
ConditionalPrefix ConditionalPrefix
| ParenthesizedCondition | ParenthesizedCondition
| StringLiteral | AnyStringLiteral
; ;
``` ```
@ -144,6 +159,8 @@ Expression:
```ebnf ```ebnf
Primary: Primary:
FunctionCall FunctionCall
| AnyStringLiteral
| LuaCode
; ;
``` ```

View File

@ -9,6 +9,8 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
ParseError(#[from] crate::syntax::error::Error), ParseError(#[from] crate::syntax::error::Error),
#[error(transparent)] #[error(transparent)]
SemanticError(#[from] crate::semantic::error::Error),
#[error(transparent)]
TranspileError(#[from] crate::transpile::TranspileError), TranspileError(#[from] crate::transpile::TranspileError),
#[error("An error occurred: {0}")] #[error("An error occurred: {0}")]
Other(String), Other(String),

View File

@ -254,6 +254,26 @@ impl Span {
}) })
} }
/// Create a span from the given start byte index to the end of the source file with an offset.
#[must_use]
pub fn to_end_with_offset(
source_file: Arc<SourceFile>,
start: usize,
end_offset: isize,
) -> Option<Self> {
if !source_file.content().is_char_boundary(start) {
return None;
}
Some(Self {
start,
end: source_file
.content()
.len()
.saturating_add_signed(end_offset),
source_file,
})
}
/// Get the string slice of the source code that the span represents. /// Get the string slice of the source code that the span represents.
#[must_use] #[must_use]
pub fn str(&self) -> &str { pub fn str(&self) -> &str {

View File

@ -4,6 +4,7 @@ use std::{borrow::Cow, collections::HashMap, fmt::Display, str::FromStr, sync::O
use crate::base::{ use crate::base::{
self, self,
log::SourceCodeDisplay,
source_file::{SourceElement, SourceIterator, Span}, source_file::{SourceElement, SourceIterator, Span},
Handler, Handler,
}; };
@ -145,24 +146,7 @@ pub enum Token {
DocComment(DocComment), DocComment(DocComment),
CommandLiteral(CommandLiteral), CommandLiteral(CommandLiteral),
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
} MacroStringLiteral(MacroStringLiteral),
impl Token {
/// Returns the span of the token.
#[must_use]
pub fn span(&self) -> &Span {
match self {
Self::WhiteSpaces(token) => &token.span,
Self::Identifier(token) => &token.span,
Self::Keyword(token) => &token.span,
Self::Punctuation(token) => &token.span,
Self::Numeric(token) => &token.span,
Self::Comment(token) => &token.span,
Self::DocComment(token) => &token.span,
Self::CommandLiteral(token) => &token.span,
Self::StringLiteral(token) => &token.span,
}
}
} }
impl SourceElement for Token { impl SourceElement for Token {
@ -177,6 +161,7 @@ impl SourceElement for Token {
Self::DocComment(token) => token.span(), Self::DocComment(token) => token.span(),
Self::CommandLiteral(token) => token.span(), Self::CommandLiteral(token) => token.span(),
Self::StringLiteral(token) => token.span(), Self::StringLiteral(token) => token.span(),
Self::MacroStringLiteral(token) => token.span(),
} }
} }
} }
@ -290,6 +275,76 @@ impl SourceElement for StringLiteral {
} }
} }
/// Represents a hardcoded macro string literal value in the source code.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MacroStringLiteral {
/// The backtick that starts the macro string literal.
starting_backtick: Punctuation,
/// The parts that make up the macro string literal.
parts: Vec<MacroStringLiteralPart>,
/// The backtick that ends the macro string literal.
ending_backtick: Punctuation,
}
impl MacroStringLiteral {
/// Returns the string content without escapement characters, leading and trailing double quotes.
#[cfg(feature = "shulkerbox")]
#[must_use]
pub fn str_content(&self) -> String {
use std::fmt::Write;
let mut content = String::new();
for part in &self.parts {
match part {
MacroStringLiteralPart::Text(span) => {
content += &crate::util::unescape_macro_string(span.str());
}
MacroStringLiteralPart::MacroUsage { identifier, .. } => {
write!(
content,
"$({})",
crate::transpile::util::identifier_to_macro(identifier.span.str())
)
.expect("can always write to string");
}
}
}
content
}
/// Returns the parts that make up the macro string literal.
#[must_use]
pub fn parts(&self) -> &[MacroStringLiteralPart] {
&self.parts
}
}
impl SourceElement for MacroStringLiteral {
fn span(&self) -> Span {
self.starting_backtick
.span
.join(&self.ending_backtick.span)
.expect("Invalid macro string literal span")
}
}
/// Represents a part of a macro string literal value in the source code.
#[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MacroStringLiteralPart {
Text(Span),
MacroUsage {
dollar: Punctuation,
open_brace: Punctuation,
identifier: Identifier,
close_brace: Punctuation,
},
}
/// Is an enumeration representing the two kinds of comments in the Shulkerscript programming language. /// Is an enumeration representing the two kinds of comments in the Shulkerscript programming language.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -362,7 +417,7 @@ impl CommandLiteral {
} }
/// Is an error that can occur when invoking the [`Token::tokenize`] method. /// Is an error that can occur when invoking the [`Token::tokenize`] method.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error, From)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub enum TokenizeError { pub enum TokenizeError {
#[error("encountered a fatal lexical error that causes the process to stop.")] #[error("encountered a fatal lexical error that causes the process to stop.")]
@ -370,8 +425,95 @@ pub enum TokenizeError {
#[error("the iterator argument is at the end of the source code.")] #[error("the iterator argument is at the end of the source code.")]
EndOfSourceCodeIteratorArgument, EndOfSourceCodeIteratorArgument,
#[error(transparent)]
InvalidMacroNameCharacter(#[from] InvalidMacroNameCharacter),
#[error(transparent)]
UnclosedMacroUsage(#[from] UnclosedMacroUsage),
#[error(transparent)]
EmptyMacroUsage(#[from] EmptyMacroUsage),
} }
/// Is an error that can occur when the macro name contains invalid characters.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InvalidMacroNameCharacter {
/// The span of the invalid characters.
pub span: Span,
}
impl Display for InvalidMacroNameCharacter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
base::log::Message::new(base::log::Severity::Error, format!("The macro name contains invalid characters: `{}`. Only alphanumeric characters and underscores are allowed.", self.span.str()))
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, Option::<u8>::None)
)
}
}
impl std::error::Error for InvalidMacroNameCharacter {}
/// Is an error that can occur when the macro usage is not closed.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UnclosedMacroUsage {
/// The span of the unclosed macro usage.
pub span: Span,
}
impl Display for UnclosedMacroUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
base::log::Message::new(
base::log::Severity::Error,
"A macro usage was opened with `$(` but never closed."
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, Option::<u8>::None)
)
}
}
impl std::error::Error for UnclosedMacroUsage {}
/// Is an error that can occur when the macro usage is not closed.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EmptyMacroUsage {
/// The span of the unclosed macro usage.
pub span: Span,
}
impl Display for EmptyMacroUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
base::log::Message::new(
base::log::Severity::Error,
"A macro usage was opened with `$(` but closed immediately with `)`."
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, Option::<u8>::None)
)
}
}
impl std::error::Error for EmptyMacroUsage {}
impl Token { impl Token {
/// Increments the iterator while the predicate returns true. /// Increments the iterator while the predicate returns true.
pub fn walk_iter(iter: &mut SourceIterator, predicate: impl Fn(char) -> bool) { pub fn walk_iter(iter: &mut SourceIterator, predicate: impl Fn(char) -> bool) {
@ -385,6 +527,7 @@ impl Token {
} }
/// Creates a span from the given start location to the current location of the iterator. /// Creates a span from the given start location to the current location of the iterator.
#[must_use]
fn create_span(start: usize, iter: &mut SourceIterator) -> Span { fn create_span(start: usize, iter: &mut SourceIterator) -> Span {
iter.peek().map_or_else( iter.peek().map_or_else(
|| Span::to_end(iter.source_file().clone(), start).unwrap(), || Span::to_end(iter.source_file().clone(), start).unwrap(),
@ -392,6 +535,26 @@ impl Token {
) )
} }
/// Creates a span from the given start location to the current location of the iterator with the given offset.
#[must_use]
fn create_span_with_end_offset(
start: usize,
iter: &mut SourceIterator,
end_offset: isize,
) -> Span {
iter.peek().map_or_else(
|| Span::to_end_with_offset(iter.source_file().clone(), start, end_offset).unwrap(),
|(index, _)| {
Span::new(
iter.source_file().clone(),
start,
index.saturating_add_signed(end_offset),
)
.unwrap()
},
)
}
/// Checks if the given character is a valid first character of an identifier. /// Checks if the given character is a valid first character of an identifier.
fn is_first_identifier_character(character: char) -> bool { fn is_first_identifier_character(character: char) -> bool {
character == '_' character == '_'
@ -551,6 +714,113 @@ impl Token {
.into() .into()
} }
/// Handles a sequence of characters that are enclosed in backticks and contain macro usages
fn handle_macro_string_literal(
iter: &mut SourceIterator,
mut start: usize,
) -> Result<Self, TokenizeError> {
let mut is_escaped = false;
let mut is_inside_macro = false;
let mut encountered_open_parenthesis = false;
let starting_backtick = Punctuation {
span: Self::create_span(start, iter),
punctuation: '`',
};
start += 1;
let mut parts = Vec::new();
while iter.peek().is_some() {
let (index, character) = iter.next().unwrap();
#[expect(clippy::collapsible_else_if)]
if is_inside_macro {
if character == ')' {
// Check if the macro usage is empty
if start + 2 == index {
return Err(EmptyMacroUsage {
span: Span::new(iter.source_file().clone(), start, index + 1).unwrap(),
}
.into());
}
parts.push(MacroStringLiteralPart::MacroUsage {
dollar: Punctuation {
span: Span::new(iter.source_file().clone(), start, start + 1).unwrap(),
punctuation: '$',
},
open_brace: Punctuation {
span: Span::new(iter.source_file().clone(), start + 1, start + 2)
.unwrap(),
punctuation: '(',
},
identifier: Identifier {
span: Self::create_span_with_end_offset(start + 2, iter, -1),
},
close_brace: Punctuation {
span: Span::new(iter.source_file().clone(), index, index + 1).unwrap(),
punctuation: ')',
},
});
start = index + 1;
is_inside_macro = false;
} else if !encountered_open_parenthesis && character == '(' {
encountered_open_parenthesis = true;
} else if encountered_open_parenthesis && !Self::is_identifier_character(character)
{
if character == '`' {
return Err(UnclosedMacroUsage {
span: Span::new(iter.source_file().clone(), start, start + 2).unwrap(),
}
.into());
}
Self::walk_iter(iter, |c| c != ')' && !Self::is_identifier_character(c));
return Err(InvalidMacroNameCharacter {
span: Self::create_span(index, iter),
}
.into());
}
} else {
if character == '$' && iter.peek().is_some_and(|(_, c)| c == '(') {
parts.push(MacroStringLiteralPart::Text(
Self::create_span_with_end_offset(start, iter, -1),
));
start = index;
is_inside_macro = true;
encountered_open_parenthesis = false;
} else if character == '\\' {
is_escaped = !is_escaped;
} else if character == '`' && !is_escaped {
if start != index {
parts.push(MacroStringLiteralPart::Text(
Self::create_span_with_end_offset(start, iter, -1),
));
}
start = index;
break;
} else {
is_escaped = false;
}
}
}
if is_inside_macro {
Err(UnclosedMacroUsage {
span: Span::new(iter.source_file().clone(), start, start + 2).unwrap(),
}
.into())
} else {
Ok(MacroStringLiteral {
starting_backtick,
parts,
ending_backtick: Punctuation {
span: Self::create_span(start, iter),
punctuation: '`',
},
}
.into())
}
}
/// Handles a command that is preceeded by a slash /// Handles a command that is preceeded by a slash
fn handle_command_literal(iter: &mut SourceIterator, start: usize) -> Self { fn handle_command_literal(iter: &mut SourceIterator, start: usize) -> Self {
Self::walk_iter(iter, |c| !(c.is_whitespace() && c.is_ascii_control())); Self::walk_iter(iter, |c| !(c.is_whitespace() && c.is_ascii_control()));
@ -592,9 +862,15 @@ impl Token {
// Found comment/single slash punctuation // Found comment/single slash punctuation
else if character == '/' { else if character == '/' {
Self::handle_comment(iter, start, character, prev_token, handler) Self::handle_comment(iter, start, character, prev_token, handler)
} else if character == '"' { }
// Found string literal
else if character == '"' {
Ok(Self::handle_string_literal(iter, start)) Ok(Self::handle_string_literal(iter, start))
} }
// Found macro string literal
else if character == '`' {
Self::handle_macro_string_literal(iter, start)
}
// Found numeric literal // Found numeric literal
else if character.is_ascii_digit() { else if character.is_ascii_digit() {
Ok(Self::handle_numeric_literal(iter, start)) Ok(Self::handle_numeric_literal(iter, start))

View File

@ -5,10 +5,13 @@ use std::{fmt::Debug, sync::Arc};
use derive_more::{Deref, From}; use derive_more::{Deref, From};
use enum_as_inner::EnumAsInner; use enum_as_inner::EnumAsInner;
use crate::base::{ use crate::{
self, base::{
source_file::{SourceElement, SourceFile, Span}, self,
Handler, source_file::{SourceElement, SourceFile, Span},
Handler,
},
lexical::Error,
}; };
use super::{ use super::{
@ -62,6 +65,17 @@ impl TokenStream {
Err(TokenizeError::FatalLexicalError) => { Err(TokenizeError::FatalLexicalError) => {
tracing::error!("Fatal lexical error encountered while tokenizing source code"); tracing::error!("Fatal lexical error encountered while tokenizing source code");
} }
Err(TokenizeError::InvalidMacroNameCharacter(err)) => {
handler.receive(Error::TokenizeError(
TokenizeError::InvalidMacroNameCharacter(err),
));
}
Err(TokenizeError::UnclosedMacroUsage(err)) => {
handler.receive(Error::TokenizeError(TokenizeError::UnclosedMacroUsage(err)));
}
Err(TokenizeError::EmptyMacroUsage(err)) => {
handler.receive(Error::TokenizeError(TokenizeError::EmptyMacroUsage(err)));
}
} }
} }
@ -184,7 +198,7 @@ pub enum TokenTree {
impl SourceElement for TokenTree { impl SourceElement for TokenTree {
fn span(&self) -> Span { fn span(&self) -> Span {
match self { match self {
Self::Token(token) => token.span().to_owned(), Self::Token(token) => token.span(),
Self::Delimited(delimited) => delimited Self::Delimited(delimited) => delimited
.open .open
.span() .span()

View File

@ -17,8 +17,10 @@ pub use shulkerbox;
pub mod base; pub mod base;
pub mod lexical; pub mod lexical;
pub mod semantic;
pub mod syntax; pub mod syntax;
pub mod transpile; pub mod transpile;
pub mod util;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
pub(crate) mod serde; pub(crate) mod serde;
@ -106,6 +108,8 @@ pub fn parse(
)); ));
} }
program.analyze_semantics(handler)?;
Ok(program) Ok(program)
} }

286
src/semantic/error.rs Normal file
View File

@ -0,0 +1,286 @@
//! Error types for the semantic analysis phase of the compiler.
#![allow(missing_docs)]
use std::{collections::HashSet, fmt::Display};
use getset::Getters;
use itertools::Itertools as _;
use crate::{
base::{
log::{Message, Severity, SourceCodeDisplay},
source_file::{SourceElement as _, Span},
},
lexical::token::StringLiteral,
syntax::syntax_tree::expression::Expression,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
MissingFunctionDeclaration(#[from] MissingFunctionDeclaration),
#[error(transparent)]
UnexpectedExpression(#[from] UnexpectedExpression),
#[error(transparent)]
ConflictingFunctionNames(#[from] ConflictingFunctionNames),
#[error(transparent)]
InvalidNamespaceName(#[from] InvalidNamespaceName),
#[error(transparent)]
UnresolvedMacroUsage(#[from] UnresolvedMacroUsage),
#[error(transparent)]
IncompatibleFunctionAnnotation(#[from] IncompatibleFunctionAnnotation),
}
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct MissingFunctionDeclaration {
#[get = "pub"]
span: Span,
#[get = "pub"]
alternatives: Vec<String>,
}
impl MissingFunctionDeclaration {
pub(super) fn from_context(identifier_span: Span, functions: &HashSet<String>) -> Self {
let own_name = identifier_span.str();
let alternatives = functions
.iter()
.filter_map(|function_name| {
let normalized_distance =
strsim::normalized_damerau_levenshtein(own_name, function_name);
(normalized_distance > 0.8
|| strsim::damerau_levenshtein(own_name, function_name) < 3)
.then_some((normalized_distance, function_name))
})
.sorted_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(_, data)| data)
.take(8)
.cloned()
.collect::<Vec<_>>();
Self {
alternatives,
span: identifier_span,
}
}
}
impl Display for MissingFunctionDeclaration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write;
let message = format!(
"no matching function declaration found for invocation of function `{}`",
self.span.str()
);
write!(f, "{}", Message::new(Severity::Error, message))?;
let help_message = if self.alternatives.is_empty() {
None
} else {
let mut message = String::from("did you mean ");
for (i, alternative) in self.alternatives.iter().enumerate() {
if i > 0 {
message.push_str(", ");
}
write!(message, "`{alternative}`")?;
}
Some(message + "?")
};
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, help_message.as_ref())
)
}
}
impl std::error::Error for MissingFunctionDeclaration {}
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnexpectedExpression(pub Expression);
impl Display for UnexpectedExpression {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(Severity::Error, "encountered unexpected expression")
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.0.span(), Option::<u8>::None)
)
}
}
impl std::error::Error for UnexpectedExpression {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConflictingFunctionNames {
pub definition: Span,
pub name: String,
}
impl Display for ConflictingFunctionNames {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!("the following function declaration conflicts with an existing function with name `{}`", self.name)
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.definition, Option::<u8>::None)
)
}
}
impl std::error::Error for ConflictingFunctionNames {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InvalidNamespaceName {
pub name: StringLiteral,
pub invalid_chars: String,
}
impl Display for InvalidNamespaceName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!(
"Invalid characters in namespace `{}`. The following characters are not allowed in namespace definitions: `{}`",
self.name.str_content(),
self.invalid_chars
)
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.name.span, Option::<u8>::None)
)
}
}
impl std::error::Error for InvalidNamespaceName {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnresolvedMacroUsage {
pub span: Span,
}
impl Display for UnresolvedMacroUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!(
"Macro `{}` was used, but could not be resolved.",
self.span.str(),
)
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(
&self.span,
Some(format!(
"You might want to add `{}` to the function parameters.",
self.span.str()
))
)
)
}
}
impl std::error::Error for UnresolvedMacroUsage {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IncompatibleFunctionAnnotation {
pub span: Span,
pub reason: String,
}
impl Display for IncompatibleFunctionAnnotation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!(
"Annotation `{}` cannot be used here, because {}.",
self.span.str(),
self.reason
)
)
)?;
write!(f, "\n{}", SourceCodeDisplay::new(&self.span, None::<u8>))
}
}
impl std::error::Error for IncompatibleFunctionAnnotation {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InvalidFunctionArguments {
pub span: Span,
pub expected: usize,
pub actual: usize,
}
impl Display for InvalidFunctionArguments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!(
"Expected {} arguments, but got {}.",
self.expected, self.actual
)
)
)?;
let help_message = if self.expected > self.actual {
format!(
"You might want to add {} more arguments.",
self.expected - self.actual
)
} else {
format!(
"You might want to remove {} arguments.",
self.actual - self.expected
)
};
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, Some(help_message))
)
}
}
impl std::error::Error for InvalidFunctionArguments {}

574
src/semantic/mod.rs Normal file
View File

@ -0,0 +1,574 @@
//! This module contains the semantic analysis of the AST.
#![allow(clippy::missing_errors_doc)]
use std::collections::HashSet;
use error::{
IncompatibleFunctionAnnotation, InvalidNamespaceName, MissingFunctionDeclaration,
UnexpectedExpression, UnresolvedMacroUsage,
};
use crate::{
base::{self, source_file::SourceElement as _, Handler},
lexical::token::{MacroStringLiteral, MacroStringLiteralPart},
syntax::syntax_tree::{
condition::{
BinaryCondition, Condition, ParenthesizedCondition, PrimaryCondition, UnaryCondition,
},
declaration::{Declaration, Function, ImportItems},
expression::{Expression, FunctionCall, Primary},
program::{Namespace, ProgramFile},
statement::{
execute_block::{
Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockHeadItem as _,
ExecuteBlockTail,
},
Block, Grouping, Run, Semicolon, Statement,
},
AnyStringLiteral,
},
};
pub mod error;
impl ProgramFile {
/// Analyzes the semantics of the program.
pub fn analyze_semantics(
&self,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.namespace().analyze_semantics(handler)?;
let mut errs = Vec::new();
let function_names = extract_all_function_names(self.declarations(), handler)?;
for declaration in self.declarations() {
if let Err(err) = declaration.analyze_semantics(&function_names, handler) {
errs.push(err);
}
}
#[expect(clippy::option_if_let_else)]
if let Some(err) = errs.first() {
Err(err.clone())
} else {
Ok(())
}
}
}
fn extract_all_function_names(
declarations: &[Declaration],
handler: &impl Handler<base::Error>,
) -> Result<HashSet<String>, error::Error> {
let mut function_names = HashSet::new();
let mut errs = Vec::new();
for declaration in declarations {
match declaration {
Declaration::Function(func) => {
let name = func.identifier();
if function_names.contains(name.span.str()) {
let err = error::Error::from(error::ConflictingFunctionNames {
name: name.span.str().to_string(),
definition: name.span(),
});
handler.receive(err.clone());
errs.push(err);
}
function_names.insert(name.span.str().to_string());
}
Declaration::Import(imp) => match imp.items() {
ImportItems::All(_) => {
handler.receive(base::Error::Other(
"Importing all items is not yet supported.".to_string(),
));
}
ImportItems::Named(items) => {
for item in items.elements() {
if function_names.contains(item.span.str()) {
let err = error::Error::from(error::ConflictingFunctionNames {
name: item.span.str().to_string(),
definition: item.span(),
});
handler.receive(err.clone());
errs.push(err);
}
function_names.insert(item.span.str().to_string());
}
}
},
Declaration::Tag(_) => {}
}
}
#[expect(clippy::option_if_let_else)]
if let Some(err) = errs.first() {
Err(err.clone())
} else {
Ok(function_names)
}
}
impl Namespace {
/// Analyzes the semantics of the namespace.
pub fn analyze_semantics(
&self,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let name = self.namespace_name();
Self::validate_str(name.str_content().as_ref()).map_err(|invalid_chars| {
let err = error::Error::from(InvalidNamespaceName {
name: name.clone(),
invalid_chars,
});
handler.receive(err.clone());
err
})
}
}
impl Declaration {
/// Analyzes the semantics of the declaration.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Function(func) => func.analyze_semantics(function_names, handler),
Self::Import(_) | Self::Tag(_) => Ok(()),
}
}
}
impl Function {
/// Analyzes the semantics of the function.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let macro_names = if let Some(parameters) = self.parameters() {
if let Some(incompatible) = self
.annotations()
.iter()
.find(|a| ["tick", "load"].contains(&a.identifier().span.str()))
{
let err =
error::Error::IncompatibleFunctionAnnotation(IncompatibleFunctionAnnotation {
span: incompatible.identifier().span(),
reason:
"functions with the `tick` or `load` annotation cannot have parameters"
.to_string(),
});
handler.receive(err.clone());
return Err(err);
}
parameters
.elements()
.map(|el| el.span.str().to_string())
.collect()
} else {
HashSet::new()
};
self.block()
.analyze_semantics(function_names, &macro_names, handler)
}
}
impl Block {
/// Analyzes the semantics of a block.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let mut errs = Vec::new();
for statement in &self.statements {
if let Err(err) = match statement {
Statement::Block(block) => {
block.analyze_semantics(function_names, macro_names, handler)
}
Statement::DocComment(_) | Statement::LiteralCommand(_) => Ok(()),
Statement::ExecuteBlock(ex) => {
ex.analyze_semantics(function_names, macro_names, handler)
}
Statement::Grouping(group) => {
group.analyze_semantics(function_names, macro_names, handler)
}
Statement::Run(run) => run.analyze_semantics(function_names, macro_names, handler),
Statement::Semicolon(sem) => {
sem.analyze_semantics(function_names, macro_names, handler)
}
} {
errs.push(err);
};
}
#[expect(clippy::option_if_let_else)]
if let Some(err) = errs.first() {
Err(err.clone())
} else {
Ok(())
}
}
}
impl ExecuteBlock {
/// Analyzes the semantics of the execute block.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::HeadTail(head, tail) => {
let head_res = head.analyze_semantics(function_names, macro_names, handler);
let tail_res = tail.analyze_semantics(function_names, macro_names, handler);
if head_res.is_err() {
head_res
} else {
tail_res
}
}
Self::IfElse(cond, then, el) => {
let cond_res = cond.analyze_semantics(function_names, macro_names, handler);
let then_res = then.analyze_semantics(function_names, macro_names, handler);
let else_res = el.analyze_semantics(function_names, macro_names, handler);
if cond_res.is_err() {
cond_res
} else if then_res.is_err() {
then_res
} else {
else_res
}
}
}
}
}
impl Grouping {
/// Analyzes the semantics of the grouping.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.block()
.analyze_semantics(function_names, macro_names, handler)
}
}
impl Run {
/// Analyzes the semantics of the run statement.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.expression()
.analyze_semantics(function_names, macro_names, handler)
}
}
impl Semicolon {
/// Analyzes the semantics of the semicolon statement.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self.expression() {
Expression::Primary(Primary::FunctionCall(func)) => {
func.analyze_semantics(function_names, macro_names, handler)
}
Expression::Primary(unexpected) => {
let error = error::Error::UnexpectedExpression(UnexpectedExpression(
Expression::Primary(unexpected.clone()),
));
handler.receive(error.clone());
Err(error)
}
}
}
}
impl ExecuteBlockHead {
/// Analyzes the semantics of the execute block head.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Align(align) => align.analyze_semantics(macro_names, handler),
Self::Anchored(anchored) => anchored.analyze_semantics(macro_names, handler),
Self::As(r#as) => r#as.analyze_semantics(macro_names, handler),
Self::At(at) => at.analyze_semantics(macro_names, handler),
Self::AsAt(asat) => asat.analyze_semantics(macro_names, handler),
Self::Conditional(cond) => cond.analyze_semantics(function_names, macro_names, handler),
Self::Facing(facing) => facing.analyze_semantics(macro_names, handler),
Self::In(r#in) => r#in.analyze_semantics(macro_names, handler),
Self::On(on) => on.analyze_semantics(macro_names, handler),
Self::Positioned(pos) => pos.analyze_semantics(macro_names, handler),
Self::Rotated(rot) => rot.analyze_semantics(macro_names, handler),
Self::Store(store) => store.analyze_semantics(macro_names, handler),
Self::Summon(summon) => summon.analyze_semantics(macro_names, handler),
}
}
}
impl ExecuteBlockTail {
/// Analyzes the semantics of the execute block tail.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Block(block) => block.analyze_semantics(function_names, macro_names, handler),
Self::ExecuteBlock(_, ex) => ex.analyze_semantics(function_names, macro_names, handler),
}
}
}
impl Conditional {
/// Analyzes the semantics of the conditional.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.condition()
.analyze_semantics(function_names, macro_names, handler)
}
}
impl ParenthesizedCondition {
/// Analyzes the semantics of the parenthesized condition.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.condition
.analyze_semantics(function_names, macro_names, handler)
}
}
impl Condition {
/// Analyzes the semantics of the condition.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Primary(prim) => prim.analyze_semantics(function_names, macro_names, handler),
Self::Binary(bin) => bin.analyze_semantics(function_names, macro_names, handler),
}
}
}
impl Else {
/// Analyzes the semantics of the else block.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.block()
.analyze_semantics(function_names, macro_names, handler)
}
}
impl MacroStringLiteral {
/// Analyzes the semantics of the macro string literal.
pub fn analyze_semantics(
&self,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let mut errors = Vec::new();
for part in self.parts() {
if let MacroStringLiteralPart::MacroUsage { identifier, .. } = part {
if !macro_names.contains(identifier.span.str()) {
let err = error::Error::UnresolvedMacroUsage(UnresolvedMacroUsage {
span: identifier.span(),
});
handler.receive(err.clone());
errors.push(err);
}
}
}
#[expect(clippy::option_if_let_else)]
if let Some(err) = errors.first() {
Err(err.clone())
} else {
Ok(())
}
}
}
impl Expression {
/// Analyzes the semantics of an expression.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Primary(prim) => prim.analyze_semantics(function_names, macro_names, handler),
}
}
}
impl Primary {
/// Analyzes the semantics of a primary expression.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::FunctionCall(func) => {
func.analyze_semantics(function_names, macro_names, handler)
}
Self::Lua(_) | Self::StringLiteral(_) => Ok(()),
Self::MacroStringLiteral(literal) => literal.analyze_semantics(macro_names, handler),
}
}
}
impl FunctionCall {
/// Analyzes the semantics of a function call.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let mut errors = Vec::new();
if !function_names.contains(self.identifier().span.str()) {
let err = error::Error::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_context(self.identifier().span(), function_names),
);
handler.receive(err.clone());
errors.push(err);
}
for expression in self
.arguments()
.iter()
.flat_map(super::syntax::syntax_tree::ConnectedList::elements)
{
if let Err(err) = expression.analyze_semantics(function_names, macro_names, handler) {
handler.receive(err.clone());
errors.push(err);
}
}
#[expect(clippy::option_if_let_else)]
if let Some(err) = errors.first() {
Err(err.clone())
} else {
Ok(())
}
}
}
impl AnyStringLiteral {
/// Analyzes the semantics of any string literal.
pub fn analyze_semantics(
&self,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::StringLiteral(_) => Ok(()),
Self::MacroStringLiteral(literal) => literal.analyze_semantics(macro_names, handler),
}
}
}
impl PrimaryCondition {
/// Analyzes the semantics of a primary condition.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
match self {
Self::Parenthesized(paren) => {
paren.analyze_semantics(function_names, macro_names, handler)
}
Self::StringLiteral(_) => Ok(()),
Self::Unary(unary) => unary.analyze_semantics(function_names, macro_names, handler),
}
}
}
impl UnaryCondition {
/// Analyzes the semantics of an unary condition.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
self.operand()
.analyze_semantics(function_names, macro_names, handler)
}
}
impl BinaryCondition {
/// Analyzes the semantics of a binary condition.
pub fn analyze_semantics(
&self,
function_names: &HashSet<String>,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), error::Error> {
let a = self
.left_operand()
.analyze_semantics(function_names, macro_names, handler)
.inspect_err(|err| {
handler.receive(err.clone());
});
let b = self
.right_operand()
.analyze_semantics(function_names, macro_names, handler)
.inspect_err(|err| {
handler.receive(err.clone());
});
if a.is_err() {
a
} else {
b
}
}
}

View File

@ -5,7 +5,7 @@ use std::fmt::Display;
use crate::{ use crate::{
base::{ base::{
log::{Message, Severity, SourceCodeDisplay}, log::{Message, Severity, SourceCodeDisplay},
source_file::Span, source_file::{SourceElement as _, Span},
}, },
lexical::token::{KeywordKind, Token}, lexical::token::{KeywordKind, Token},
}; };
@ -34,6 +34,8 @@ pub enum SyntaxKind {
Declaration, Declaration,
Numeric, Numeric,
StringLiteral, StringLiteral,
MacroStringLiteral,
AnyStringLiteral,
Statement, Statement,
Expression, Expression,
Type, Type,
@ -69,6 +71,8 @@ impl SyntaxKind {
Self::Declaration => "a declaration token".to_string(), Self::Declaration => "a declaration token".to_string(),
Self::Numeric => "a numeric token".to_string(), Self::Numeric => "a numeric token".to_string(),
Self::StringLiteral => "a string literal".to_string(), Self::StringLiteral => "a string literal".to_string(),
Self::MacroStringLiteral => "a macro string literal".to_string(),
Self::AnyStringLiteral => "a (macro) string literal".to_string(),
Self::Statement => "a statement syntax".to_string(), Self::Statement => "a statement syntax".to_string(),
Self::Expression => "an expression syntax".to_string(), Self::Expression => "an expression syntax".to_string(),
Self::Type => "a type syntax".to_string(), Self::Type => "a type syntax".to_string(),
@ -105,6 +109,7 @@ impl Display for UnexpectedSyntax {
Some(Token::Numeric(..)) => "a numeric token".to_string(), Some(Token::Numeric(..)) => "a numeric token".to_string(),
Some(Token::CommandLiteral(..)) => "a literal command token".to_string(), Some(Token::CommandLiteral(..)) => "a literal command token".to_string(),
Some(Token::StringLiteral(..)) => "a string literal token".to_string(), Some(Token::StringLiteral(..)) => "a string literal token".to_string(),
Some(Token::MacroStringLiteral(..)) => "a macro string literal token".to_string(),
None => "EOF".to_string(), None => "EOF".to_string(),
}; };
@ -117,7 +122,7 @@ impl Display for UnexpectedSyntax {
write!( write!(
f, f,
"\n{}", "\n{}",
SourceCodeDisplay::new(span.span(), Option::<u8>::None) SourceCodeDisplay::new(&span.span(), Option::<u8>::None)
) )
}) })
} }

View File

@ -6,12 +6,18 @@ use enum_as_inner::EnumAsInner;
use crate::{ use crate::{
base::{self, Handler}, base::{self, Handler},
lexical::{ lexical::{
token::{Identifier, Keyword, KeywordKind, Numeric, Punctuation, StringLiteral, Token}, token::{
Identifier, Keyword, KeywordKind, MacroStringLiteral, Numeric, Punctuation,
StringLiteral, Token,
},
token_stream::{Delimited, Delimiter, TokenStream, TokenTree}, token_stream::{Delimited, Delimiter, TokenStream, TokenTree},
}, },
}; };
use super::error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax}; use super::{
error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax},
syntax_tree::AnyStringLiteral,
};
/// Represents a parser that reads a token stream and constructs an abstract syntax tree. /// Represents a parser that reads a token stream and constructs an abstract syntax tree.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut)]
@ -432,6 +438,49 @@ impl<'a> Frame<'a> {
} }
} }
/// Expects the next [`Token`] to be an [`MacroStringLiteral`], and returns it.
///
/// # Errors
/// If the next [`Token`] is not an [`MacroStringLiteral`].
pub fn parse_macro_string_literal(
&mut self,
handler: &impl Handler<base::Error>,
) -> ParseResult<MacroStringLiteral> {
match self.next_significant_token() {
Reading::Atomic(Token::MacroStringLiteral(literal)) => Ok(literal),
found => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::MacroStringLiteral,
found: found.into_token(),
});
handler.receive(err.clone());
Err(err)
}
}
}
/// Expects the next [`Token`] to be an [`AnyStringLiteral`], and returns it.
///
/// # Errors
/// If the next [`Token`] is not an [`AnyStringLiteral`].
pub fn parse_any_string_literal(
&mut self,
handler: &impl Handler<base::Error>,
) -> ParseResult<AnyStringLiteral> {
match self.next_significant_token() {
Reading::Atomic(Token::StringLiteral(literal)) => Ok(literal.into()),
Reading::Atomic(Token::MacroStringLiteral(literal)) => Ok(literal.into()),
found => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::AnyStringLiteral,
found: found.into_token(),
});
handler.receive(err.clone());
Err(err)
}
}
}
/// Expects the next [`Token`] to be a [`Keyword`] of specific kind, and returns it. /// Expects the next [`Token`] to be a [`Keyword`] of specific kind, and returns it.
/// ///
/// # Errors /// # Errors

View File

@ -14,7 +14,7 @@ use crate::{
Handler, VoidHandler, Handler, VoidHandler,
}, },
lexical::{ lexical::{
token::{Punctuation, StringLiteral, Token}, token::{Punctuation, Token},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::{ syntax::{
@ -23,6 +23,8 @@ use crate::{
}, },
}; };
use super::AnyStringLiteral;
/// Condition that is viewed as a single entity during precedence parsing. /// Condition that is viewed as a single entity during precedence parsing.
/// ///
/// Syntax Synopsis: /// Syntax Synopsis:
@ -31,7 +33,7 @@ use crate::{
/// PrimaryCondition: /// PrimaryCondition:
/// UnaryCondition /// UnaryCondition
/// | ParenthesizedCondition /// | ParenthesizedCondition
/// | StringLiteral /// | AnyStringLiteral
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -39,7 +41,7 @@ use crate::{
pub enum PrimaryCondition { pub enum PrimaryCondition {
Unary(UnaryCondition), Unary(UnaryCondition),
Parenthesized(ParenthesizedCondition), Parenthesized(ParenthesizedCondition),
StringLiteral(StringLiteral), StringLiteral(AnyStringLiteral),
} }
impl SourceElement for PrimaryCondition { impl SourceElement for PrimaryCondition {
@ -354,7 +356,13 @@ impl<'a> Parser<'a> {
// string literal // string literal
Reading::Atomic(Token::StringLiteral(literal)) => { Reading::Atomic(Token::StringLiteral(literal)) => {
self.forward(); self.forward();
Ok(PrimaryCondition::StringLiteral(literal)) Ok(PrimaryCondition::StringLiteral(literal.into()))
}
// macro string literal
Reading::Atomic(Token::MacroStringLiteral(literal)) => {
self.forward();
Ok(PrimaryCondition::StringLiteral(literal.into()))
} }
// parenthesized condition // parenthesized condition

View File

@ -10,7 +10,9 @@ use crate::{
Handler, Handler,
}, },
lexical::{ lexical::{
token::{Identifier, Keyword, KeywordKind, Punctuation, StringLiteral, Token}, token::{
Identifier, Keyword, KeywordKind, MacroStringLiteral, Punctuation, StringLiteral, Token,
},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::{ syntax::{
@ -52,6 +54,9 @@ impl SourceElement for Expression {
/// ``` ebnf /// ``` ebnf
/// Primary: /// Primary:
/// FunctionCall /// FunctionCall
/// | StringLiteral
/// | MacroStringLiteral
/// | LuaCode
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -59,6 +64,7 @@ impl SourceElement for Expression {
pub enum Primary { pub enum Primary {
FunctionCall(FunctionCall), FunctionCall(FunctionCall),
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
MacroStringLiteral(MacroStringLiteral),
Lua(Box<LuaCode>), Lua(Box<LuaCode>),
} }
@ -67,6 +73,7 @@ impl SourceElement for Primary {
match self { match self {
Self::FunctionCall(function_call) => function_call.span(), Self::FunctionCall(function_call) => function_call.span(),
Self::StringLiteral(string_literal) => string_literal.span(), Self::StringLiteral(string_literal) => string_literal.span(),
Self::MacroStringLiteral(macro_string_literal) => macro_string_literal.span(),
Self::Lua(lua_code) => lua_code.span(), Self::Lua(lua_code) => lua_code.span(),
} }
} }
@ -180,6 +187,7 @@ impl<'a> Parser<'a> {
/// # Errors /// # Errors
/// - If the parser is not at a primary expression. /// - If the parser is not at a primary expression.
/// - If the parser is not at a valid primary expression. /// - If the parser is not at a valid primary expression.
#[expect(clippy::too_many_lines)]
pub fn parse_primary(&mut self, handler: &impl Handler<base::Error>) -> ParseResult<Primary> { pub fn parse_primary(&mut self, handler: &impl Handler<base::Error>) -> ParseResult<Primary> {
match self.stop_at_significant() { match self.stop_at_significant() {
// identifier expression // identifier expression
@ -224,6 +232,14 @@ impl<'a> Parser<'a> {
Ok(Primary::StringLiteral(literal)) Ok(Primary::StringLiteral(literal))
} }
// macro string literal expression
Reading::Atomic(Token::MacroStringLiteral(macro_string_literal)) => {
// eat the macro string literal
self.forward();
Ok(Primary::MacroStringLiteral(macro_string_literal))
}
// lua code expression // lua code expression
Reading::Atomic(Token::Keyword(lua_keyword)) Reading::Atomic(Token::Keyword(lua_keyword))
if lua_keyword.keyword == KeywordKind::Lua => if lua_keyword.keyword == KeywordKind::Lua =>
@ -267,10 +283,11 @@ impl<'a> Parser<'a> {
let combined = first let combined = first
.into_token() .into_token()
.and_then(|first| { .and_then(|first| {
first.span().join(&last.into_token().map_or_else( first.span().join(
|| first.span().to_owned(), &last
|last| last.span().to_owned(), .into_token()
)) .map_or_else(|| first.span(), |last| last.span()),
)
}) })
.expect("Invalid lua code span"); .expect("Invalid lua code span");

View File

@ -1,5 +1,6 @@
//! Contains the syntax tree nodes that represent the structure of the source code. //! Contains the syntax tree nodes that represent the structure of the source code.
use derive_more::derive::From;
use getset::Getters; use getset::Getters;
use crate::{ use crate::{
@ -9,7 +10,7 @@ use crate::{
Handler, VoidHandler, Handler, VoidHandler,
}, },
lexical::{ lexical::{
token::{Punctuation, Token}, token::{MacroStringLiteral, Punctuation, StringLiteral, Token},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::parser::Reading, syntax::parser::Reading,
@ -64,6 +65,29 @@ pub struct DelimitedList<T> {
pub close: Punctuation, pub close: Punctuation,
} }
/// Represents a syntax tree node that can be either a string literal or a macro string literal.
///
/// Syntax Synopsis:
/// ```ebnf
/// AnyStringLiteral: StringLiteral | MacroStringLiteral ;
/// ```
#[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, From)]
pub enum AnyStringLiteral {
StringLiteral(StringLiteral),
MacroStringLiteral(MacroStringLiteral),
}
impl SourceElement for AnyStringLiteral {
fn span(&self) -> Span {
match self {
Self::StringLiteral(string_literal) => string_literal.span(),
Self::MacroStringLiteral(macro_string_literal) => macro_string_literal.span(),
}
}
}
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
/// Parses a list of elements enclosed by a pair of delimiters, separated by a separator. /// Parses a list of elements enclosed by a pair of delimiters, separated by a separator.
/// ///

View File

@ -12,7 +12,7 @@ use crate::{
lexical::token::{Keyword, KeywordKind, Punctuation, StringLiteral, Token}, lexical::token::{Keyword, KeywordKind, Punctuation, StringLiteral, Token},
syntax::{ syntax::{
self, self,
error::{InvalidArgument, ParseResult, SyntaxKind, UnexpectedSyntax}, error::{ParseResult, SyntaxKind, UnexpectedSyntax},
parser::{Parser, Reading}, parser::{Parser, Reading},
}, },
}; };
@ -108,22 +108,7 @@ impl<'a> Parser<'a> {
// eat the keyword // eat the keyword
self.forward(); self.forward();
let namespace_name = self.parse_string_literal(handler).and_then(|name| { let namespace_name = self.parse_string_literal(handler)?;
Namespace::validate_str(name.str_content().as_ref())
.map(|()| name.clone())
.map_err(|invalid| {
let err = syntax::error::Error::InvalidArgument(InvalidArgument {
message: format!(
"Invalid characters in namespace '{}'. The following characters are not allowed in namespace definitions: '{}'",
name.str_content(),
invalid
),
span: name.span(),
});
handler.receive(err.clone());
err
})
})?;
let semicolon = self.parse_punctuation(';', true, handler)?; let semicolon = self.parse_punctuation(';', true, handler)?;

View File

@ -1,5 +1,7 @@
//! Execute block statement syntax tree. //! Execute block statement syntax tree.
use std::collections::HashSet;
use derive_more::From; use derive_more::From;
use enum_as_inner::EnumAsInner; use enum_as_inner::EnumAsInner;
use getset::Getters; use getset::Getters;
@ -11,13 +13,13 @@ use crate::{
Handler, VoidHandler, Handler, VoidHandler,
}, },
lexical::{ lexical::{
token::{Keyword, KeywordKind, Punctuation, StringLiteral, Token}, token::{Keyword, KeywordKind, Punctuation, Token},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::{ syntax::{
error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax}, error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax},
parser::{DelimitedTree, Parser, Reading}, parser::{DelimitedTree, Parser, Reading},
syntax_tree::condition::ParenthesizedCondition, syntax_tree::{condition::ParenthesizedCondition, AnyStringLiteral},
}, },
}; };
@ -217,7 +219,7 @@ impl SourceElement for Else {
/// ///
/// ```ebnf /// ```ebnf
/// As: /// As:
/// 'as' '(' StringLiteral ')' /// 'as' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -231,7 +233,7 @@ pub struct As {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the as statement. /// The selector of the as statement.
#[get = "pub"] #[get = "pub"]
as_selector: StringLiteral, as_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -248,7 +250,7 @@ impl SourceElement for As {
impl As { impl As {
/// Dissolves the [`As`] into its components. /// Dissolves the [`As`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.as_keyword, self.as_keyword,
self.open_paren, self.open_paren,
@ -263,7 +265,7 @@ impl As {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Align: /// Align:
/// 'align' '(' StringLiteral ')' /// 'align' '(' AnyStringLiteral ')'
/// ; /// ;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)]
@ -276,7 +278,7 @@ pub struct Align {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the align statement. /// The selector of the align statement.
#[get = "pub"] #[get = "pub"]
align_selector: StringLiteral, align_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -294,7 +296,7 @@ impl SourceElement for Align {
impl Align { impl Align {
/// Dissolves the [`Align`] into its components. /// Dissolves the [`Align`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.align_keyword, self.align_keyword,
self.open_paren, self.open_paren,
@ -309,7 +311,7 @@ impl Align {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Anchored: /// Anchored:
/// 'anchored' '(' StringLiteral ')' /// 'anchored' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -323,7 +325,7 @@ pub struct Anchored {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the anchored statement. /// The selector of the anchored statement.
#[get = "pub"] #[get = "pub"]
anchored_selector: StringLiteral, anchored_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -339,7 +341,7 @@ impl SourceElement for Anchored {
impl Anchored { impl Anchored {
/// Dissolves the [`Anchored`] into its components. /// Dissolves the [`Anchored`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.anchored_keyword, self.anchored_keyword,
self.open_paren, self.open_paren,
@ -354,7 +356,7 @@ impl Anchored {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// AsAt: /// AsAt:
/// 'asat' '(' StringLiteral ')' /// 'asat' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -368,7 +370,7 @@ pub struct AsAt {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the asat statement. /// The selector of the asat statement.
#[get = "pub"] #[get = "pub"]
asat_selector: StringLiteral, asat_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -384,7 +386,7 @@ impl SourceElement for AsAt {
impl AsAt { impl AsAt {
/// Dissolves the [`AsAt`] into its components. /// Dissolves the [`AsAt`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.asat_keyword, self.asat_keyword,
self.open_paren, self.open_paren,
@ -399,7 +401,7 @@ impl AsAt {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// At: /// At:
/// 'at' '(' StringLiteral ')' /// 'at' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -413,7 +415,7 @@ pub struct At {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the at statement. /// The selector of the at statement.
#[get = "pub"] #[get = "pub"]
at_selector: StringLiteral, at_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -429,7 +431,7 @@ impl SourceElement for At {
impl At { impl At {
/// Dissolves the [`At`] into its components. /// Dissolves the [`At`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.at_keyword, self.at_keyword,
self.open_paren, self.open_paren,
@ -444,7 +446,7 @@ impl At {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Facing: /// Facing:
/// 'facing' '(' StringLiteral ')' /// 'facing' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -458,7 +460,7 @@ pub struct Facing {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the facing statement. /// The selector of the facing statement.
#[get = "pub"] #[get = "pub"]
facing_selector: StringLiteral, facing_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -474,7 +476,7 @@ impl SourceElement for Facing {
impl Facing { impl Facing {
/// Dissolves the [`Facing`] into its components. /// Dissolves the [`Facing`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.facing_keyword, self.facing_keyword,
self.open_paren, self.open_paren,
@ -489,7 +491,7 @@ impl Facing {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// In: /// In:
/// 'in' '(' StringLiteral ')' /// 'in' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -503,7 +505,7 @@ pub struct In {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the in statement. /// The selector of the in statement.
#[get = "pub"] #[get = "pub"]
in_selector: StringLiteral, in_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -519,7 +521,7 @@ impl SourceElement for In {
impl In { impl In {
/// Dissolves the [`In`] into its components. /// Dissolves the [`In`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.in_keyword, self.in_keyword,
self.open_paren, self.open_paren,
@ -534,7 +536,7 @@ impl In {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// On: /// On:
/// 'on' '(' StringLiteral ')' /// 'on' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -548,7 +550,7 @@ pub struct On {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the on statement. /// The selector of the on statement.
#[get = "pub"] #[get = "pub"]
on_selector: StringLiteral, on_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -564,7 +566,7 @@ impl SourceElement for On {
impl On { impl On {
/// Dissolves the [`On`] into its components. /// Dissolves the [`On`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.on_keyword, self.on_keyword,
self.open_paren, self.open_paren,
@ -579,7 +581,7 @@ impl On {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Positioned: /// Positioned:
/// 'positioned' '(' StringLiteral ')' /// 'positioned' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -593,7 +595,7 @@ pub struct Positioned {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the positioned statement. /// The selector of the positioned statement.
#[get = "pub"] #[get = "pub"]
positioned_selector: StringLiteral, positioned_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -609,7 +611,7 @@ impl SourceElement for Positioned {
impl Positioned { impl Positioned {
/// Dissolves the [`Positioned`] into its components. /// Dissolves the [`Positioned`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.positioned_keyword, self.positioned_keyword,
self.open_paren, self.open_paren,
@ -624,7 +626,7 @@ impl Positioned {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Rotated: /// Rotated:
/// 'rotated' '(' StringLiteral ')' /// 'rotated' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -638,7 +640,7 @@ pub struct Rotated {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the rotated statement. /// The selector of the rotated statement.
#[get = "pub"] #[get = "pub"]
rotated_selector: StringLiteral, rotated_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -654,7 +656,7 @@ impl SourceElement for Rotated {
impl Rotated { impl Rotated {
/// Dissolves the [`Rotated`] into its components. /// Dissolves the [`Rotated`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.rotated_keyword, self.rotated_keyword,
self.open_paren, self.open_paren,
@ -669,7 +671,7 @@ impl Rotated {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Store: /// Store:
/// 'store' '(' StringLiteral ')' /// 'store' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -683,7 +685,7 @@ pub struct Store {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the store statement. /// The selector of the store statement.
#[get = "pub"] #[get = "pub"]
store_selector: StringLiteral, store_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -699,7 +701,7 @@ impl SourceElement for Store {
impl Store { impl Store {
/// Dissolves the [`Store`] into its components. /// Dissolves the [`Store`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.store_keyword, self.store_keyword,
self.open_paren, self.open_paren,
@ -714,7 +716,7 @@ impl Store {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// Summon: /// Summon:
/// 'summon' '(' StringLiteral ')' /// 'summon' '(' AnyStringLiteral ')'
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -728,7 +730,7 @@ pub struct Summon {
open_paren: Punctuation, open_paren: Punctuation,
/// The selector of the summon statement. /// The selector of the summon statement.
#[get = "pub"] #[get = "pub"]
summon_selector: StringLiteral, summon_selector: AnyStringLiteral,
/// The close parenthesis. /// The close parenthesis.
#[get = "pub"] #[get = "pub"]
close_paren: Punctuation, close_paren: Punctuation,
@ -744,7 +746,7 @@ impl SourceElement for Summon {
impl Summon { impl Summon {
/// Dissolves the [`Summon`] into its components. /// Dissolves the [`Summon`] into its components.
#[must_use] #[must_use]
pub fn dissolve(self) -> (Keyword, Punctuation, StringLiteral, Punctuation) { pub fn dissolve(self) -> (Keyword, Punctuation, AnyStringLiteral, Punctuation) {
( (
self.summon_keyword, self.summon_keyword,
self.open_paren, self.open_paren,
@ -825,7 +827,7 @@ impl<'a> Parser<'a> {
let argument = match self.stop_at_significant() { let argument = match self.stop_at_significant() {
Reading::IntoDelimited(punc) if punc.punctuation == '(' => self.step_into( Reading::IntoDelimited(punc) if punc.punctuation == '(' => self.step_into(
Delimiter::Parenthesis, Delimiter::Parenthesis,
|parser| parser.parse_string_literal(handler), |parser| parser.parse_any_string_literal(handler),
handler, handler,
), ),
unexpected => { unexpected => {
@ -896,7 +898,7 @@ impl<'a> Parser<'a> {
fn head_from_keyword( fn head_from_keyword(
keyword: Keyword, keyword: Keyword,
argument: DelimitedTree<StringLiteral>, argument: DelimitedTree<AnyStringLiteral>,
) -> ParseResult<ExecuteBlockHead> { ) -> ParseResult<ExecuteBlockHead> {
Ok(match keyword.keyword { Ok(match keyword.keyword {
KeywordKind::Align => Align { KeywordKind::Align => Align {
@ -986,3 +988,91 @@ fn head_from_keyword(
_ => unreachable!("The keyword is not a valid execute block head."), _ => unreachable!("The keyword is not a valid execute block head."),
}) })
} }
/// Trait for the execute block head items with a [`AnyStringLiteral`] as their selector.
pub trait ExecuteBlockHeadItem {
/// Returns a reference to the selector of the execute block head item.
fn selector(&self) -> &AnyStringLiteral;
/// Analyzes the semantics of the execute block head item.
#[expect(clippy::missing_errors_doc)]
fn analyze_semantics(
&self,
macro_names: &HashSet<String>,
handler: &impl Handler<base::Error>,
) -> Result<(), crate::semantic::error::Error> {
self.selector().analyze_semantics(macro_names, handler)
}
}
impl ExecuteBlockHeadItem for Align {
fn selector(&self) -> &AnyStringLiteral {
&self.align_selector
}
}
impl ExecuteBlockHeadItem for Anchored {
fn selector(&self) -> &AnyStringLiteral {
&self.anchored_selector
}
}
impl ExecuteBlockHeadItem for As {
fn selector(&self) -> &AnyStringLiteral {
&self.as_selector
}
}
impl ExecuteBlockHeadItem for At {
fn selector(&self) -> &AnyStringLiteral {
&self.at_selector
}
}
impl ExecuteBlockHeadItem for AsAt {
fn selector(&self) -> &AnyStringLiteral {
&self.asat_selector
}
}
impl ExecuteBlockHeadItem for Facing {
fn selector(&self) -> &AnyStringLiteral {
&self.facing_selector
}
}
impl ExecuteBlockHeadItem for In {
fn selector(&self) -> &AnyStringLiteral {
&self.in_selector
}
}
impl ExecuteBlockHeadItem for On {
fn selector(&self) -> &AnyStringLiteral {
&self.on_selector
}
}
impl ExecuteBlockHeadItem for Positioned {
fn selector(&self) -> &AnyStringLiteral {
&self.positioned_selector
}
}
impl ExecuteBlockHeadItem for Rotated {
fn selector(&self) -> &AnyStringLiteral {
&self.rotated_selector
}
}
impl ExecuteBlockHeadItem for Store {
fn selector(&self) -> &AnyStringLiteral {
&self.store_selector
}
}
impl ExecuteBlockHeadItem for Summon {
fn selector(&self) -> &AnyStringLiteral {
&self.summon_selector
}
}

View File

@ -1,10 +1,19 @@
//! Conversion functions for converting between tokens/ast-nodes and [`shulkerbox`] types //! Conversion functions for converting between tokens/ast-nodes and [`shulkerbox`] types
use shulkerbox::datapack::Condition as DpCondition; use shulkerbox::{
datapack::Condition as DpCondition,
util::{MacroString, MacroStringPart},
};
use crate::syntax::syntax_tree::condition::{ use crate::{
BinaryCondition, Condition, ConditionalBinaryOperator, ConditionalPrefixOperator, lexical::token::{MacroStringLiteral, MacroStringLiteralPart},
PrimaryCondition, syntax::syntax_tree::{
condition::{
BinaryCondition, Condition, ConditionalBinaryOperator, ConditionalPrefixOperator,
PrimaryCondition,
},
AnyStringLiteral,
},
}; };
impl From<Condition> for DpCondition { impl From<Condition> for DpCondition {
@ -19,9 +28,7 @@ impl From<Condition> for DpCondition {
impl From<PrimaryCondition> for DpCondition { impl From<PrimaryCondition> for DpCondition {
fn from(value: PrimaryCondition) -> Self { fn from(value: PrimaryCondition) -> Self {
match value { match value {
PrimaryCondition::StringLiteral(literal) => { PrimaryCondition::StringLiteral(literal) => Self::Atom(literal.into()),
Self::Atom(literal.str_content().to_string())
}
PrimaryCondition::Parenthesized(cond) => cond.dissolve().1.into(), PrimaryCondition::Parenthesized(cond) => cond.dissolve().1.into(),
PrimaryCondition::Unary(prefix) => match prefix.operator() { PrimaryCondition::Unary(prefix) => match prefix.operator() {
ConditionalPrefixOperator::LogicalNot(_) => { ConditionalPrefixOperator::LogicalNot(_) => {
@ -32,6 +39,56 @@ impl From<PrimaryCondition> for DpCondition {
} }
} }
impl From<&AnyStringLiteral> for MacroString {
fn from(value: &AnyStringLiteral) -> Self {
match value {
AnyStringLiteral::StringLiteral(literal) => Self::from(literal.str_content().as_ref()),
AnyStringLiteral::MacroStringLiteral(literal) => Self::from(literal),
}
}
}
impl From<AnyStringLiteral> for MacroString {
fn from(value: AnyStringLiteral) -> Self {
Self::from(&value)
}
}
impl From<&MacroStringLiteral> for MacroString {
fn from(value: &MacroStringLiteral) -> Self {
if value
.parts()
.iter()
.any(|p| matches!(p, MacroStringLiteralPart::MacroUsage { .. }))
{
Self::MacroString(
value
.parts()
.iter()
.map(|part| match part {
MacroStringLiteralPart::Text(span) => MacroStringPart::String(
crate::util::unescape_macro_string(span.str()).to_string(),
),
MacroStringLiteralPart::MacroUsage { identifier, .. } => {
MacroStringPart::MacroUsage(
super::util::identifier_to_macro(identifier.span.str()).to_string(),
)
}
})
.collect(),
)
} else {
Self::String(value.str_content())
}
}
}
impl From<MacroStringLiteral> for MacroString {
fn from(value: MacroStringLiteral) -> Self {
Self::from(&value)
}
}
impl From<BinaryCondition> for DpCondition { impl From<BinaryCondition> for DpCondition {
fn from(value: BinaryCondition) -> Self { fn from(value: BinaryCondition) -> Self {
let (lhs, op, rhs) = value.dissolve(); let (lhs, op, rhs) = value.dissolve();

View File

@ -8,9 +8,9 @@ use itertools::Itertools;
use crate::{ use crate::{
base::{ base::{
log::{Message, Severity, SourceCodeDisplay}, log::{Message, Severity, SourceCodeDisplay},
source_file::{SourceElement, Span}, source_file::Span,
}, },
syntax::syntax_tree::expression::Expression, semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression},
}; };
use super::FunctionData; use super::FunctionData;
@ -29,6 +29,8 @@ pub enum TranspileError {
LuaRuntimeError(#[from] LuaRuntimeError), LuaRuntimeError(#[from] LuaRuntimeError),
#[error(transparent)] #[error(transparent)]
ConflictingFunctionNames(#[from] ConflictingFunctionNames), ConflictingFunctionNames(#[from] ConflictingFunctionNames),
#[error(transparent)]
InvalidFunctionArguments(#[from] InvalidFunctionArguments),
} }
/// The result of a transpilation operation. /// The result of a transpilation operation.
@ -144,52 +146,3 @@ impl LuaRuntimeError {
} }
} }
} }
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnexpectedExpression(pub Expression);
impl Display for UnexpectedExpression {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(Severity::Error, "encountered unexpected expression")
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.0.span(), Option::<u8>::None)
)
}
}
impl std::error::Error for UnexpectedExpression {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConflictingFunctionNames {
pub definition: Span,
pub name: String,
}
impl Display for ConflictingFunctionNames {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Message::new(
Severity::Error,
format!("the following function declaration conflicts with an existing function with name `{}`", self.name)
)
)?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.definition, Option::<u8>::None)
)
}
}
impl std::error::Error for ConflictingFunctionNames {}

View File

@ -20,13 +20,13 @@ mod transpiler;
#[cfg_attr(feature = "shulkerbox", doc(inline))] #[cfg_attr(feature = "shulkerbox", doc(inline))]
pub use transpiler::Transpiler; pub use transpiler::Transpiler;
#[cfg(feature = "shulkerbox")] pub mod util;
mod util;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct FunctionData { pub(super) struct FunctionData {
pub(super) namespace: String, pub(super) namespace: String,
pub(super) identifier_span: Span, pub(super) identifier_span: Span,
pub(super) parameters: Vec<String>,
pub(super) statements: Vec<Statement>, pub(super) statements: Vec<Statement>,
pub(super) public: bool, pub(super) public: bool,
pub(super) annotations: HashMap<String, Option<String>>, pub(super) annotations: HashMap<String, Option<String>>,

View File

@ -3,7 +3,7 @@
use chksum_md5 as md5; use chksum_md5 as md5;
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
iter, ops::Deref,
}; };
use shulkerbox::datapack::{self, Command, Datapack, Execute}; use shulkerbox::datapack::{self, Command, Datapack, Execute};
@ -14,6 +14,7 @@ use crate::{
source_file::{SourceElement, Span}, source_file::{SourceElement, Span},
Handler, Handler,
}, },
semantic::error::{ConflictingFunctionNames, InvalidFunctionArguments, UnexpectedExpression},
syntax::syntax_tree::{ syntax::syntax_tree::{
declaration::{Declaration, ImportItems}, declaration::{Declaration, ImportItems},
expression::{Expression, FunctionCall, Primary}, expression::{Expression, FunctionCall, Primary},
@ -23,11 +24,11 @@ use crate::{
Statement, Statement,
}, },
}, },
transpile::error::{ConflictingFunctionNames, MissingFunctionDeclaration}, transpile::error::MissingFunctionDeclaration,
}; };
use super::{ use super::{
error::{TranspileError, TranspileResult, UnexpectedExpression}, error::{TranspileError, TranspileResult},
FunctionData, FunctionData,
}; };
@ -97,7 +98,7 @@ impl Transpiler {
); );
for identifier_span in always_transpile_functions { for identifier_span in always_transpile_functions {
self.get_or_transpile_function(&identifier_span, handler)?; self.get_or_transpile_function(&identifier_span, None, handler)?;
} }
Ok(()) Ok(())
@ -122,7 +123,7 @@ impl Transpiler {
&mut self, &mut self,
declaration: &Declaration, declaration: &Declaration,
namespace: &Namespace, namespace: &Namespace,
_handler: &impl Handler<base::Error>, handler: &impl Handler<base::Error>,
) { ) {
let program_identifier = declaration.span().source_file().identifier().clone(); let program_identifier = declaration.span().source_file().identifier().clone();
match declaration { match declaration {
@ -148,6 +149,15 @@ impl Transpiler {
FunctionData { FunctionData {
namespace: namespace.namespace_name().str_content().to_string(), namespace: namespace.namespace_name().str_content().to_string(),
identifier_span: identifier_span.clone(), identifier_span: identifier_span.clone(),
parameters: function
.parameters()
.as_ref()
.map(|l| {
l.elements()
.map(|i| i.span.str().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default(),
statements, statements,
public: function.is_public(), public: function.is_public(),
annotations, annotations,
@ -162,12 +172,13 @@ impl Transpiler {
let aliases = &mut self.aliases; let aliases = &mut self.aliases;
match import.items() { match import.items() {
ImportItems::All(_) => todo!("Importing all items is not yet supported."), ImportItems::All(_) => {
handler.receive(base::Error::Other(
"Importing all items is not yet supported.".to_string(),
));
}
ImportItems::Named(list) => { ImportItems::Named(list) => {
let items = iter::once(list.first()) for item in list.elements() {
.chain(list.rest().iter().map(|(_, ident)| ident));
for item in items {
let name = item.span.str(); let name = item.span.str();
aliases.insert( aliases.insert(
(program_identifier.clone(), name.to_string()), (program_identifier.clone(), name.to_string()),
@ -203,8 +214,9 @@ impl Transpiler {
fn get_or_transpile_function( fn get_or_transpile_function(
&mut self, &mut self,
identifier_span: &Span, identifier_span: &Span,
arguments: Option<&[&Expression]>,
handler: &impl Handler<base::Error>, handler: &impl Handler<base::Error>,
) -> TranspileResult<String> { ) -> TranspileResult<(String, Option<BTreeMap<String, String>>)> {
let program_identifier = identifier_span.source_file().identifier(); let program_identifier = identifier_span.source_file().identifier();
let program_query = ( let program_query = (
program_identifier.to_string(), program_identifier.to_string(),
@ -244,6 +256,7 @@ impl Transpiler {
handler.receive(error.clone()); handler.receive(error.clone());
error error
})?; })?;
function_data.statements.clone() function_data.statements.clone()
}; };
let commands = self.transpile_function(&statements, program_identifier, handler)?; let commands = self.transpile_function(&statements, program_identifier, handler)?;
@ -315,10 +328,35 @@ impl Transpiler {
); );
} }
let locations = &self.function_locations; let parameters = {
locations let function_data = self
.functions
.get(&program_query)
.or_else(|| {
alias_query
.clone()
.and_then(|q| self.functions.get(&q).filter(|f| f.public))
})
.ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_context(
identifier_span.clone(),
&self.functions,
),
);
handler.receive(error.clone());
error
})?;
function_data.parameters.clone()
};
let function_location = self
.function_locations
.get(&program_query) .get(&program_query)
.or_else(|| alias_query.and_then(|q| locations.get(&q).filter(|(_, p)| *p))) .or_else(|| {
alias_query.and_then(|q| self.function_locations.get(&q).filter(|(_, p)| *p))
})
.ok_or_else(|| { .ok_or_else(|| {
let error = TranspileError::MissingFunctionDeclaration( let error = TranspileError::MissingFunctionDeclaration(
MissingFunctionDeclaration::from_context( MissingFunctionDeclaration::from_context(
@ -329,7 +367,57 @@ impl Transpiler {
handler.receive(error.clone()); handler.receive(error.clone());
error error
}) })
.map(|(s, _)| s.to_owned()) .map(|(s, _)| s.to_owned())?;
let arg_count = arguments.iter().flat_map(|x| x.iter()).count();
if arg_count != parameters.len() {
let err = TranspileError::InvalidFunctionArguments(InvalidFunctionArguments {
expected: parameters.len(),
actual: arg_count,
span: identifier_span.clone(),
});
handler.receive(err.clone());
Err(err)
} else if arg_count > 0 {
let mut compiled_args = Vec::new();
let mut errs = Vec::new();
for expression in arguments.iter().flat_map(|x| x.iter()) {
let value = match expression {
Expression::Primary(Primary::FunctionCall(func)) => self
.transpile_function_call(func, handler)
.map(|cmd| match cmd {
Command::Raw(s) => s,
_ => unreachable!("Function call should always return a raw command"),
}),
Expression::Primary(Primary::Lua(lua)) => {
lua.eval_string(handler).map(Option::unwrap_or_default)
}
Expression::Primary(Primary::StringLiteral(string)) => {
Ok(string.str_content().to_string())
}
Expression::Primary(Primary::MacroStringLiteral(literal)) => {
Ok(literal.str_content())
}
};
match value {
Ok(value) => {
compiled_args.push(value);
}
Err(err) => {
compiled_args.push(String::new());
errs.push(err.clone());
}
}
}
if let Some(err) = errs.first() {
return Err(err.clone());
}
let function_args = parameters.into_iter().zip(compiled_args).collect();
Ok((function_location, Some(function_args)))
} else {
Ok((function_location, None))
}
} }
fn transpile_function( fn transpile_function(
@ -374,6 +462,9 @@ impl Transpiler {
Expression::Primary(Primary::StringLiteral(string)) => { Expression::Primary(Primary::StringLiteral(string)) => {
Ok(Some(Command::Raw(string.str_content().to_string()))) Ok(Some(Command::Raw(string.str_content().to_string())))
} }
Expression::Primary(Primary::MacroStringLiteral(string)) => {
Ok(Some(Command::UsesMacro(string.into())))
}
Expression::Primary(Primary::Lua(code)) => { Expression::Primary(Primary::Lua(code)) => {
Ok(code.eval_string(handler)?.map(Command::Raw)) Ok(code.eval_string(handler)?.map(Command::Raw))
} }
@ -431,8 +522,29 @@ impl Transpiler {
func: &FunctionCall, func: &FunctionCall,
handler: &impl Handler<base::Error>, handler: &impl Handler<base::Error>,
) -> TranspileResult<Command> { ) -> TranspileResult<Command> {
let location = self.get_or_transpile_function(&func.identifier().span, handler)?; let arguments = func
Ok(Command::Raw(format!("function {location}"))) .arguments()
.as_ref()
.map(|l| l.elements().map(Deref::deref).collect::<Vec<_>>());
let (location, arguments) =
self.get_or_transpile_function(&func.identifier().span, arguments.as_deref(), handler)?;
let mut function_call = format!("function {location}");
if let Some(arguments) = arguments {
use std::fmt::Write;
let arguments = arguments
.iter()
.map(|(ident, v)| {
format!(
r#"{macro_name}:"{escaped}""#,
macro_name = super::util::identifier_to_macro(ident),
escaped = crate::util::escape_str(v)
)
})
.collect::<Vec<_>>()
.join(",");
write!(function_call, " {{{arguments}}}").unwrap();
}
Ok(Command::Raw(function_call))
} }
fn transpile_execute_block( fn transpile_execute_block(
@ -595,53 +707,53 @@ impl Transpiler {
None None
} }
} }
ExecuteBlockHead::As(as_) => { ExecuteBlockHead::As(r#as) => {
let selector = as_.as_selector().str_content(); let selector = r#as.as_selector();
tail.map(|tail| Execute::As(selector.to_string(), Box::new(tail))) tail.map(|tail| Execute::As(selector.into(), Box::new(tail)))
} }
ExecuteBlockHead::At(at) => { ExecuteBlockHead::At(at) => {
let selector = at.at_selector().str_content(); let selector = at.at_selector();
tail.map(|tail| Execute::At(selector.to_string(), Box::new(tail))) tail.map(|tail| Execute::At(selector.into(), Box::new(tail)))
} }
ExecuteBlockHead::Align(align) => { ExecuteBlockHead::Align(align) => {
let align = align.align_selector().str_content(); let align = align.align_selector();
tail.map(|tail| Execute::Align(align.to_string(), Box::new(tail))) tail.map(|tail| Execute::Align(align.into(), Box::new(tail)))
} }
ExecuteBlockHead::Anchored(anchored) => { ExecuteBlockHead::Anchored(anchored) => {
let anchor = anchored.anchored_selector().str_content(); let anchor = anchored.anchored_selector();
tail.map(|tail| Execute::Anchored(anchor.to_string(), Box::new(tail))) tail.map(|tail| Execute::Anchored(anchor.into(), Box::new(tail)))
} }
ExecuteBlockHead::In(in_) => { ExecuteBlockHead::In(r#in) => {
let dimension = in_.in_selector().str_content(); let dimension = r#in.in_selector();
tail.map(|tail| Execute::In(dimension.to_string(), Box::new(tail))) tail.map(|tail| Execute::In(dimension.into(), Box::new(tail)))
} }
ExecuteBlockHead::Positioned(positioned) => { ExecuteBlockHead::Positioned(positioned) => {
let position = positioned.positioned_selector().str_content(); let position = positioned.positioned_selector();
tail.map(|tail| Execute::Positioned(position.to_string(), Box::new(tail))) tail.map(|tail| Execute::Positioned(position.into(), Box::new(tail)))
} }
ExecuteBlockHead::Rotated(rotated) => { ExecuteBlockHead::Rotated(rotated) => {
let rotation = rotated.rotated_selector().str_content(); let rotation = rotated.rotated_selector();
tail.map(|tail| Execute::Rotated(rotation.to_string(), Box::new(tail))) tail.map(|tail| Execute::Rotated(rotation.into(), Box::new(tail)))
} }
ExecuteBlockHead::Facing(facing) => { ExecuteBlockHead::Facing(facing) => {
let facing = facing.facing_selector().str_content(); let facing = facing.facing_selector();
tail.map(|tail| Execute::Facing(facing.to_string(), Box::new(tail))) tail.map(|tail| Execute::Facing(facing.into(), Box::new(tail)))
} }
ExecuteBlockHead::AsAt(as_at) => { ExecuteBlockHead::AsAt(as_at) => {
let selector = as_at.asat_selector().str_content(); let selector = as_at.asat_selector();
tail.map(|tail| Execute::AsAt(selector.to_string(), Box::new(tail))) tail.map(|tail| Execute::AsAt(selector.into(), Box::new(tail)))
} }
ExecuteBlockHead::On(on) => { ExecuteBlockHead::On(on) => {
let dimension = on.on_selector().str_content(); let dimension = on.on_selector();
tail.map(|tail| Execute::On(dimension.to_string(), Box::new(tail))) tail.map(|tail| Execute::On(dimension.into(), Box::new(tail)))
} }
ExecuteBlockHead::Store(store) => { ExecuteBlockHead::Store(store) => {
let store = store.store_selector().str_content(); let store = store.store_selector();
tail.map(|tail| Execute::Store(store.to_string(), Box::new(tail))) tail.map(|tail| Execute::Store(store.into(), Box::new(tail)))
} }
ExecuteBlockHead::Summon(summon) => { ExecuteBlockHead::Summon(summon) => {
let entity = summon.summon_selector().str_content(); let entity = summon.summon_selector();
tail.map(|tail| Execute::Summon(entity.to_string(), Box::new(tail))) tail.map(|tail| Execute::Summon(entity.into(), Box::new(tail)))
} }
}) })
} }

View File

@ -1,3 +1,8 @@
//! Utility methods for transpiling
#[cfg(feature = "shulkerbox")]
use chksum_md5 as md5;
fn normalize_program_identifier<S>(identifier: S) -> String fn normalize_program_identifier<S>(identifier: S) -> String
where where
S: AsRef<str>, S: AsRef<str>,
@ -19,6 +24,8 @@ where
.join("/") .join("/")
} }
/// Calculate the identifier to import the function based on the current identifier and the import path
#[must_use]
pub fn calculate_import_identifier<S, T>(current_identifier: S, import_path: T) -> String pub fn calculate_import_identifier<S, T>(current_identifier: S, import_path: T) -> String
where where
S: AsRef<str>, S: AsRef<str>,
@ -32,3 +39,25 @@ where
normalize_program_identifier(identifier_elements.join("/") + "/" + import_path.as_ref()) normalize_program_identifier(identifier_elements.join("/") + "/" + import_path.as_ref())
} }
} }
/// Transforms an identifier to a macro name that only contains `a-zA-Z0-9_`.
#[cfg(feature = "shulkerbox")]
#[must_use]
pub fn identifier_to_macro(ident: &str) -> std::borrow::Cow<str> {
if ident.contains("__")
|| ident
.chars()
.any(|c| !(c == '_' && c.is_ascii_alphanumeric()))
{
let new_ident = ident
.chars()
.filter(|c| *c == '_' || c.is_ascii_alphanumeric())
.collect::<String>();
let chksum = md5::hash(ident).to_hex_lowercase();
std::borrow::Cow::Owned(new_ident + "__" + &chksum[..8])
} else {
std::borrow::Cow::Borrowed(ident)
}
}

82
src/util.rs Normal file
View File

@ -0,0 +1,82 @@
//! Utility functions for the `Shulkerscript` language.
use std::borrow::Cow;
/// Escapes `"` and `\` in a string.
#[must_use]
pub fn escape_str(s: &str) -> Cow<str> {
if s.contains('"') || s.contains('\\') {
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
_ => escaped.push(c),
}
}
Cow::Owned(escaped)
} else {
Cow::Borrowed(s)
}
}
/// Unescapes '\`', `\`, `\n`, `\r` and `\t` in a string.
#[must_use]
pub fn unescape_macro_string(s: &str) -> Cow<str> {
if s.contains('\\') || s.contains('`') {
Cow::Owned(
s.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t")
.replace("\\`", "`")
.replace("\\\\", "\\"),
)
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_str() {
assert_eq!(escape_str("Hello, world!"), "Hello, world!");
assert_eq!(escape_str(r#"Hello, "world"!"#), r#"Hello, \"world\"!"#);
assert_eq!(escape_str(r"Hello, \world\!"), r"Hello, \\world\\!");
}
#[test]
fn test_unescape_macro_string() {
assert_eq!(unescape_macro_string("Hello, world!"), "Hello, world!");
assert_eq!(
unescape_macro_string(r#"Hello, "world"!"#),
r#"Hello, "world"!"#
);
assert_eq!(
unescape_macro_string(r"Hello, \world\!"),
r"Hello, \world\!"
);
assert_eq!(
unescape_macro_string(r"Hello, \nworld\!"),
"Hello, \nworld\\!"
);
assert_eq!(
unescape_macro_string(r"Hello, \rworld\!"),
"Hello, \rworld\\!"
);
assert_eq!(
unescape_macro_string(r"Hello, \tworld\!"),
"Hello, \tworld\\!"
);
assert_eq!(
unescape_macro_string(r"Hello, \`world\!"),
r"Hello, `world\!"
);
assert_eq!(
unescape_macro_string(r"Hello, \\world\!"),
r"Hello, \world\!"
);
}
}

View File

@ -28,9 +28,9 @@ fn transpile_test1() {
main_fn.add_command(Command::Raw("say Hello, World!".to_string())); main_fn.add_command(Command::Raw("say Hello, World!".to_string()));
let exec_cmd = Command::Execute(Execute::As( let exec_cmd = Command::Execute(Execute::As(
"@a".to_string(), "@a".to_string().into(),
Box::new(Execute::If( Box::new(Execute::If(
Condition::Atom("entity @p[distance=..5]".to_string()), Condition::Atom("entity @p[distance=..5]".to_string().into()),
Box::new(Execute::Run(Box::new(Command::Raw( Box::new(Execute::Run(Box::new(Command::Raw(
"say You are close to me!".to_string(), "say You are close to me!".to_string(),
)))), )))),