Compare commits

...

3 Commits

Author SHA1 Message Date
Moritz Hölting b7d50f8222 allow escaping of $ in macro string 2025-08-26 21:15:41 +02:00
Moritz Hölting 183d3e85c6 implement new tokenizer & parser for TemplateStringLiteral 2025-08-25 19:12:20 +02:00
Moritz Hölting 08ed56b673 rename MacroString to TemplateString 2025-08-25 15:51:21 +02:00
20 changed files with 854 additions and 569 deletions

View File

@ -170,7 +170,7 @@ VariableDeclarationAssignment:
## AnyStringLiteral ## AnyStringLiteral
```ebnf ```ebnf
AnyStringLiteral: StringLiteral | MacroStringLiteral ; AnyStringLiteral: StringLiteral | TemplateStringLiteral ;
``` ```
## AnnotationValue ## AnnotationValue
@ -230,11 +230,11 @@ Expression:
Primary | Binary ; Primary | Binary ;
``` ```
## MacroStringLiteral ## TemplateStringLiteral
```ebnf ```ebnf
MacroStringLiteral: TemplateStringLiteral:
'`' ( TEXT | '$(' [a-zA-Z0-9_]+ ')' )* '`'; '`' ( TemplateStringLiteralText | '$(' Expression ')' )* '`';
``` ```
## Else ## Else
@ -304,10 +304,17 @@ Primary:
| StringLiteral | StringLiteral
| FunctionCall | FunctionCall
| MemberAccess | MemberAccess
| MacroStringLiteral | TemplateStringLiteral
| LuaCode | LuaCode
``` ```
## TemplateStringLiteralText
```ebnf
TemplateStringLiteralText:
TEXT ;
```
## Align ## Align
```ebnf ```ebnf

View File

@ -3,7 +3,7 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
fmt::Debug, fmt::Debug,
iter::{Iterator, Peekable}, iter::Iterator,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::CharIndices, str::CharIndices,
@ -11,6 +11,7 @@ use std::{
}; };
use getset::{CopyGetters, Getters}; use getset::{CopyGetters, Getters};
use itertools::{structs::MultiPeek, Itertools as _};
use super::{file_provider::FileProvider, Error}; use super::{file_provider::FileProvider, Error};
@ -72,8 +73,9 @@ impl SourceFile {
pub fn iter(self: &Arc<Self>) -> SourceIterator<'_> { pub fn iter(self: &Arc<Self>) -> SourceIterator<'_> {
SourceIterator { SourceIterator {
source_file: self, source_file: self,
iterator: self.content().char_indices().peekable(), iterator: self.content().char_indices().multipeek(),
prev: None, prev: None,
in_template_string_expression_open_count: Vec::new(),
} }
} }
@ -337,16 +339,69 @@ pub struct SourceIterator<'a> {
/// Get the source file that the iterator is iterating over. /// Get the source file that the iterator is iterating over.
#[get_copy = "pub"] #[get_copy = "pub"]
source_file: &'a Arc<SourceFile>, source_file: &'a Arc<SourceFile>,
iterator: Peekable<CharIndices<'a>>, iterator: MultiPeek<CharIndices<'a>>,
/// Get the previous character that was iterated over. /// Get the previous character that was iterated over.
#[get_copy = "pub"] #[get_copy = "pub"]
prev: Option<(usize, char)>, prev: Option<(usize, char)>,
/// Current state for parsing template strings.
in_template_string_expression_open_count: Vec<u32>,
} }
impl SourceIterator<'_> { impl SourceIterator<'_> {
/// Peek at the next character in the source file. /// Peek at the next character in the source file.
pub fn peek(&mut self) -> Option<(usize, char)> { pub fn peek(&mut self) -> Option<(usize, char)> {
self.iterator.reset_peek();
self.iterator.peek().copied() self.iterator.peek().copied()
} }
/// Peek at the next character in the source file.
pub fn multipeek(&mut self) -> Option<(usize, char)> {
self.iterator.peek().copied()
}
/// Reset the multipeek state of the iterator.
pub fn reset_multipeek(&mut self) {
self.iterator.reset_peek();
}
/// Increase the count of open parentheses in the current template string expression.
pub fn increase_template_string_expression_open_paren_count(&mut self) {
if let Some(count) = self.in_template_string_expression_open_count.last_mut() {
*count += 1;
}
}
/// Decrease the count of open parentheses in the current template string expression.
pub fn decrease_template_string_expression_open_paren_count(&mut self) {
if let Some(count) = self.in_template_string_expression_open_count.last_mut() {
*count = count.saturating_sub(1);
}
}
/// Enter a template string expression.
pub fn enter_template_string(&mut self) {
self.in_template_string_expression_open_count.push(0);
}
/// Exit a template string expression.
pub fn exit_template_string(&mut self) {
self.in_template_string_expression_open_count.pop();
}
/// Check if the iterator is currently in a template string expression.
#[must_use]
pub fn is_in_template_string_expression(&self) -> Option<bool> {
self.in_template_string_expression_open_count
.last()
.map(|&count| count > 0)
}
/// Get the number of open parentheses in the current template string expression.
#[must_use]
pub fn template_string_expression_open_paren_count(&self) -> Option<u32> {
self.in_template_string_expression_open_count
.last()
.copied()
}
} }
impl Iterator for SourceIterator<'_> { impl Iterator for SourceIterator<'_> {
type Item = (usize, char); type Item = (usize, char);

View File

@ -10,7 +10,6 @@ use std::{
use crate::base::{ use crate::base::{
self, self,
log::SourceCodeDisplay,
source_file::{SourceElement, SourceIterator, Span}, source_file::{SourceElement, SourceIterator, Span},
Handler, Handler,
}; };
@ -165,7 +164,7 @@ pub enum Token {
DocComment(DocComment), DocComment(DocComment),
CommandLiteral(CommandLiteral), CommandLiteral(CommandLiteral),
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
MacroStringLiteral(Box<MacroStringLiteral>), TemplateStringText(TemplateStringLiteralText),
} }
impl SourceElement for Token { impl SourceElement for Token {
@ -181,7 +180,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(), Self::TemplateStringText(token) => token.span(),
} }
} }
} }
@ -352,80 +351,25 @@ impl SourceElement for StringLiteral {
} }
} }
/// Represents a hardcoded macro string literal value in the source code. /// Represents a hardcoded template string text value in the source code.
/// ///
/// ```ebnf /// ```ebnf
/// MacroStringLiteral: /// TemplateStringLiteralText:
/// '`' ( TEXT | '$(' [a-zA-Z0-9_]+ ')' )* '`'; /// TEXT ;
/// ``` /// ```
#[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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MacroStringLiteral { pub struct TemplateStringLiteralText {
/// The backtick that starts the macro string literal. /// Is the span that makes up the token.
starting_backtick: Punctuation, pub span: Span,
/// 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 { impl SourceElement for TemplateStringLiteralText {
/// Returns the string content without escapement characters, leading and trailing double quotes.
#[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::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 { fn span(&self) -> Span {
self.starting_backtick self.span.clone()
.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)]
@ -500,101 +444,15 @@ 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, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
#[allow(missing_docs)] #[allow(missing_docs)]
#[expect(missing_copy_implementations)]
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.")]
FatalLexicalError, FatalLexicalError,
#[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) {
@ -616,26 +474,6 @@ 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 == '_'
@ -792,110 +630,84 @@ impl Token {
.into() .into()
} }
/// Handles a sequence of characters that are enclosed in backticks and contain macro usages /// Handles a backticks for opening and closing template strings
fn handle_macro_string_literal( fn handle_template_string_quotes(iter: &mut SourceIterator, start: usize) -> Self {
iter: &mut SourceIterator, if iter
mut start: usize, .is_in_template_string_expression()
) -> Result<Self, TokenizeError> { .is_some_and(|last| !last)
let mut is_escaped = false; {
let mut is_inside_macro = false; // in template string text
let mut encountered_open_parenthesis = false; iter.exit_template_string();
let starting_backtick = Punctuation { } else {
span: Self::create_span(start, iter), // outside template string or in expression
punctuation: '`', iter.enter_template_string();
};
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 { Punctuation {
Err(UnclosedMacroUsage { span: Self::create_span(start, iter),
span: Span::new(iter.source_file().clone(), start, start + 2).unwrap(), punctuation: '`',
}
.into()
}
fn handle_template_string_inner(
iter: &mut SourceIterator,
start: usize,
character: char,
prev_token: Option<&Self>,
) -> Self {
let prev_was_backslash = iter.prev().is_some_and(|(_, c)| c == '\\');
if !prev_was_backslash && character == '$' && iter.peek().is_some_and(|(_, c)| c == '(') {
// starts immediately with expression, return punctuation
return Punctuation {
span: Self::create_span(start, iter),
punctuation: '$',
} }
.into()) .into();
} else { }
Ok(Box::new(MacroStringLiteral {
starting_backtick, match (character, prev_token) {
parts, ('(', Some(Self::Punctuation(punc)))
ending_backtick: Punctuation { if !prev_was_backslash && punc.punctuation == '$' =>
{
// Found expression opening parenthesis
iter.increase_template_string_expression_open_paren_count();
return Punctuation {
span: Self::create_span(start, iter), span: Self::create_span(start, iter),
punctuation: '`', punctuation: '(',
}, }
}) .into();
.into()) }
_ => {}
}
loop {
if character != '`' {
iter.reset_multipeek();
Self::walk_iter(iter, |c| !matches!(c, '$' | '`' | '\\'));
}
iter.reset_multipeek();
let first_peek = iter.multipeek().map(|(_, c)| c);
let second_peek_open_paren = iter.multipeek().is_some_and(|(_, c)| c == '(');
if first_peek.is_some_and(|c| c == '\\') {
iter.next();
iter.next();
}
if character == '`' || first_peek.is_none_or(|c| c == '`') || second_peek_open_paren {
// Found expression start, end of text
break TemplateStringLiteralText {
span: Self::create_span(start, iter),
}
.into();
}
iter.next();
} }
} }
@ -929,8 +741,13 @@ impl Token {
.next() .next()
.ok_or(TokenizeError::EndOfSourceCodeIteratorArgument)?; .ok_or(TokenizeError::EndOfSourceCodeIteratorArgument)?;
if iter.is_in_template_string_expression().is_some_and(|b| !b) && character != '`' {
Ok(Self::handle_template_string_inner(
iter, start, character, prev_token,
))
}
// Found white spaces // Found white spaces
if character.is_whitespace() { else if character.is_whitespace() {
Ok(Self::handle_whitespace(iter, start)) Ok(Self::handle_whitespace(iter, start))
} }
// Found identifier/keyword // Found identifier/keyword
@ -947,7 +764,7 @@ impl Token {
} }
// Found macro string literal // Found macro string literal
else if character == '`' { else if character == '`' {
Self::handle_macro_string_literal(iter, start) Ok(Self::handle_template_string_quotes(iter, start))
} }
// Found integer literal // Found integer literal
else if character.is_ascii_digit() { else if character.is_ascii_digit() {
@ -955,6 +772,12 @@ impl Token {
} }
// Found a punctuation // Found a punctuation
else if character.is_ascii_punctuation() { else if character.is_ascii_punctuation() {
if character == '(' {
iter.increase_template_string_expression_open_paren_count();
} else if character == ')' {
iter.decrease_template_string_expression_open_paren_count();
}
Ok(Punctuation { Ok(Punctuation {
span: Self::create_span(start, iter), span: Self::create_span(start, iter),
punctuation: character, punctuation: character,

View File

@ -5,13 +5,10 @@ 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::{ use crate::base::{
base::{ self,
self, source_file::{SourceElement, SourceFile, Span},
source_file::{SourceElement, SourceFile, Span}, Handler,
Handler,
},
lexical::Error,
}; };
use super::{ use super::{
@ -65,17 +62,6 @@ 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)));
}
} }
} }

View File

@ -4,10 +4,13 @@
use crate::{ use crate::{
base::{self, source_file::SourceElement as _, Handler}, base::{self, source_file::SourceElement as _, Handler},
lexical::token::{KeywordKind, MacroStringLiteral, MacroStringLiteralPart}, lexical::token::KeywordKind,
syntax::syntax_tree::{ syntax::syntax_tree::{
declaration::{Declaration, Function, FunctionVariableType, ImportItems}, declaration::{Declaration, Function, FunctionVariableType, ImportItems},
expression::{Binary, BinaryOperator, Expression, LuaCode, PrefixOperator, Primary}, expression::{
Binary, BinaryOperator, Expression, LuaCode, PrefixOperator, Primary,
TemplateStringLiteral, TemplateStringLiteralPart,
},
program::{Namespace, ProgramFile}, program::{Namespace, ProgramFile},
statement::{ statement::{
execute_block::{ execute_block::{
@ -581,7 +584,7 @@ impl Primary {
} }
} }
Self::Boolean(_) | Self::Integer(_) | Self::StringLiteral(_) => Ok(()), Self::Boolean(_) | Self::Integer(_) | Self::StringLiteral(_) => Ok(()),
Self::MacroStringLiteral(lit) => lit.analyze_semantics(scope, handler), Self::TemplateStringLiteral(lit) => lit.analyze_semantics(scope, handler),
Self::FunctionCall(call) => { Self::FunctionCall(call) => {
let var = scope.get_variable(call.identifier().span.str()); let var = scope.get_variable(call.identifier().span.str());
var.map_or_else( var.map_or_else(
@ -719,7 +722,7 @@ impl Primary {
match self { match self {
Self::Boolean(_) => expected == ValueType::Boolean, Self::Boolean(_) => expected == ValueType::Boolean,
Self::Integer(_) => expected == ValueType::Integer, Self::Integer(_) => expected == ValueType::Integer,
Self::StringLiteral(_) | Self::MacroStringLiteral(_) => { Self::StringLiteral(_) | Self::TemplateStringLiteral(_) => {
matches!(expected, ValueType::String | ValueType::Boolean) matches!(expected, ValueType::String | ValueType::Boolean)
} }
Self::FunctionCall(_) => matches!(expected, ValueType::Boolean | ValueType::Integer), Self::FunctionCall(_) => matches!(expected, ValueType::Boolean | ValueType::Integer),
@ -948,12 +951,12 @@ impl AnyStringLiteral {
) -> Result<(), error::Error> { ) -> Result<(), error::Error> {
match self { match self {
Self::StringLiteral(_) => Ok(()), Self::StringLiteral(_) => Ok(()),
Self::MacroStringLiteral(lit) => lit.analyze_semantics(scope, handler), Self::TemplateStringLiteral(lit) => lit.analyze_semantics(scope, handler),
} }
} }
} }
impl MacroStringLiteral { impl TemplateStringLiteral {
fn analyze_semantics( fn analyze_semantics(
&self, &self,
scope: &SemanticScope, scope: &SemanticScope,
@ -963,25 +966,76 @@ impl MacroStringLiteral {
for part in self.parts() { for part in self.parts() {
match part { match part {
MacroStringLiteralPart::MacroUsage { identifier, .. } => { TemplateStringLiteralPart::Expression { expression, .. } => {
if let Some(variable_type) = scope.get_variable(identifier.span.str()) { match expression.as_ref() {
if variable_type != VariableType::MacroParameter { Expression::Primary(Primary::Identifier(identifier)) => {
let err = if let Some(variable_type) = scope.get_variable(identifier.span.str()) {
error::Error::UnexpectedExpression(UnexpectedExpression(Box::new( // TODO: template string correct checks
Expression::Primary(Primary::Identifier(identifier.clone())), // if variable_type != VariableType::MacroParameter {
))); // let err = error::Error::UnexpectedExpression(UnexpectedExpression(
// Box::new(Expression::Primary(Primary::Identifier(
// identifier.clone(),
// ))),
// ));
// handler.receive(err.clone());
// errs.push(err);
// }
} else {
let err = error::Error::UnknownIdentifier(UnknownIdentifier {
identifier: identifier.span.clone(),
});
handler.receive(err.clone());
errs.push(err);
}
}
Expression::Primary(Primary::Indexed(indexed)) => {
if let Primary::Identifier(identifier) = indexed.object().as_ref() {
if let Some(variable_type) =
scope.get_variable(identifier.span.str())
{
match variable_type {
VariableType::BooleanStorageArray
| VariableType::ScoreboardArray
| VariableType::Tag
| VariableType::Scoreboard => {
// Valid types
}
_ => {
let err = error::Error::UnexpectedExpression(
UnexpectedExpression(expression.clone()),
);
handler.receive(err.clone());
errs.push(err);
}
}
} else {
let err = error::Error::UnknownIdentifier(UnknownIdentifier {
identifier: identifier.span.clone(),
});
handler.receive(err.clone());
errs.push(err);
}
} else {
let err = error::Error::UnexpectedExpression(UnexpectedExpression(
expression.clone(),
));
handler.receive(err.clone());
errs.push(err);
}
if let Err(err) = indexed.index().analyze_semantics(scope, handler) {
errs.push(err);
}
}
_ => {
let err = error::Error::UnexpectedExpression(UnexpectedExpression(
expression.clone(),
));
handler.receive(err.clone()); handler.receive(err.clone());
errs.push(err); errs.push(err);
} }
} else {
let err = error::Error::UnknownIdentifier(UnknownIdentifier {
identifier: identifier.span.clone(),
});
handler.receive(err.clone());
errs.push(err);
} }
} }
MacroStringLiteralPart::Text(_) => {} TemplateStringLiteralPart::Text(_) => {}
} }
} }

View File

@ -275,14 +275,8 @@ mod tests {
serde_json::from_str::<SerdeWrapper<ProgramFile>>(&serialized).unwrap(); serde_json::from_str::<SerdeWrapper<ProgramFile>>(&serialized).unwrap();
assert_eq!( assert_eq!(
Arc::as_ptr( Arc::as_ptr(deserialized.namespace().keyword().span.source_file()),
deserialized Arc::as_ptr(deserialized.namespace().name().span.source_file())
.namespace()
.namespace_keyword()
.span
.source_file()
),
Arc::as_ptr(deserialized.namespace().namespace_name().span.source_file())
); );
} }
} }

View File

@ -37,7 +37,7 @@ pub enum SyntaxKind {
Integer, Integer,
Boolean, Boolean,
StringLiteral, StringLiteral,
MacroStringLiteral, TemplateStringLiteralPart,
AnyStringLiteral, AnyStringLiteral,
Statement, Statement,
Expression, Expression,
@ -76,8 +76,8 @@ impl SyntaxKind {
Self::Integer => "an integer token".to_string(), Self::Integer => "an integer token".to_string(),
Self::Boolean => "a boolean token".to_string(), Self::Boolean => "a boolean 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::TemplateStringLiteralPart => "part of a template string literal".to_string(),
Self::AnyStringLiteral => "a (macro) string literal".to_string(), Self::AnyStringLiteral => "a (template) 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::Operator => "an operator".to_string(), Self::Operator => "an operator".to_string(),
@ -116,7 +116,7 @@ impl Display for UnexpectedSyntax {
Some(Token::Boolean(..)) => "a boolean token".to_string(), Some(Token::Boolean(..)) => "a boolean 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(), Some(Token::TemplateStringText(..)) => "a template string text token".to_string(),
None => "EOF".to_string(), None => "EOF".to_string(),
}; };

View File

@ -6,18 +6,12 @@ use enum_as_inner::EnumAsInner;
use crate::{ use crate::{
base::{self, Handler}, base::{self, Handler},
lexical::{ lexical::{
token::{ token::{Identifier, Integer, Keyword, KeywordKind, Punctuation, StringLiteral, Token},
Identifier, Integer, Keyword, KeywordKind, MacroStringLiteral, Punctuation,
StringLiteral, Token,
},
token_stream::{Delimited, Delimiter, TokenStream, TokenTree}, token_stream::{Delimited, Delimiter, TokenStream, TokenTree},
}, },
}; };
use super::{ use super::error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax};
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)]
@ -438,49 +432,6 @@ impl Frame<'_> {
} }
} }
/// 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(Box::new(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(Box::new(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

@ -344,7 +344,6 @@ impl Parser<'_> {
/// ///
/// # Errors /// # Errors
/// - cannot parse declaration from current position /// - cannot parse declaration from current position
#[expect(clippy::too_many_lines)]
#[tracing::instrument(level = "trace", skip_all)] #[tracing::instrument(level = "trace", skip_all)]
pub fn parse_declaration( pub fn parse_declaration(
&mut self, &mut self,

View File

@ -13,15 +13,16 @@ use crate::{
}, },
lexical::{ lexical::{
token::{ token::{
Boolean, Identifier, Integer, Keyword, KeywordKind, MacroStringLiteral, Punctuation, Boolean, Identifier, Integer, Keyword, KeywordKind, Punctuation, StringLiteral,
StringLiteral, Token, TemplateStringLiteralText, Token,
}, },
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::{ syntax::{
self, self,
error::{Error, ParseResult, UnexpectedSyntax}, error::{Error, ParseResult, SyntaxKind, UnexpectedSyntax},
parser::{Parser, Reading}, parser::{Parser, Reading},
syntax_tree::AnyStringLiteral,
}, },
}; };
@ -177,7 +178,7 @@ impl SourceElement for Expression {
/// | StringLiteral /// | StringLiteral
/// | FunctionCall /// | FunctionCall
/// | MemberAccess /// | MemberAccess
/// | MacroStringLiteral /// | TemplateStringLiteral
/// | LuaCode /// | LuaCode
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
@ -193,7 +194,7 @@ pub enum Primary {
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
FunctionCall(FunctionCall), FunctionCall(FunctionCall),
MemberAccess(MemberAccess), MemberAccess(MemberAccess),
MacroStringLiteral(MacroStringLiteral), TemplateStringLiteral(TemplateStringLiteral),
Lua(Box<LuaCode>), Lua(Box<LuaCode>),
} }
@ -209,7 +210,7 @@ impl SourceElement for Primary {
Self::StringLiteral(string_literal) => string_literal.span(), Self::StringLiteral(string_literal) => string_literal.span(),
Self::FunctionCall(function_call) => function_call.span(), Self::FunctionCall(function_call) => function_call.span(),
Self::MemberAccess(member_access) => member_access.span(), Self::MemberAccess(member_access) => member_access.span(),
Self::MacroStringLiteral(macro_string_literal) => macro_string_literal.span(), Self::TemplateStringLiteral(template_string_literal) => template_string_literal.span(),
Self::Lua(lua_code) => lua_code.span(), Self::Lua(lua_code) => lua_code.span(),
} }
} }
@ -391,6 +392,70 @@ impl SourceElement for FunctionCall {
} }
} }
/// Represents a hardcoded template string literal value in the source code.
///
/// ```ebnf
/// TemplateStringLiteral:
/// '`' ( TemplateStringLiteralText | '$(' Expression ')' )* '`';
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TemplateStringLiteral {
/// The backtick that starts the template string literal.
pub(crate) starting_backtick: Punctuation,
/// The parts that make up the template string literal.
pub(crate) parts: Vec<TemplateStringLiteralPart>,
/// The backtick that ends the template string literal.
pub(crate) ending_backtick: Punctuation,
}
impl TemplateStringLiteral {
/// Returns the parts that make up the template string literal.
#[must_use]
pub fn parts(&self) -> &[TemplateStringLiteralPart] {
&self.parts
}
}
impl SourceElement for TemplateStringLiteral {
fn span(&self) -> Span {
self.starting_backtick
.span
.join(&self.ending_backtick.span)
.expect("Invalid template string literal span")
}
}
/// Represents a part of a template 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 TemplateStringLiteralPart {
Text(TemplateStringLiteralText),
Expression {
dollar: Punctuation,
open_brace: Punctuation,
expression: Box<Expression>,
close_brace: Punctuation,
},
}
impl SourceElement for TemplateStringLiteralPart {
fn span(&self) -> Span {
match self {
Self::Text(text) => text.span(),
Self::Expression {
dollar,
close_brace,
..
} => dollar
.span()
.join(&close_brace.span())
.expect("Invalid template string literal part span"),
}
}
}
/// Represents a lua code block in the syntax tree. /// Represents a lua code block in the syntax tree.
/// ///
/// Syntax Synopsis: /// Syntax Synopsis:
@ -589,6 +654,13 @@ impl Parser<'_> {
})) }))
} }
// template string literal
Reading::Atomic(Token::Punctuation(punc)) if punc.punctuation == '`' => {
let template_string_literal = self.parse_template_string_literal(handler)?;
Ok(Primary::TemplateStringLiteral(template_string_literal))
}
// parenthesized expression // parenthesized expression
Reading::IntoDelimited(left_parenthesis) if left_parenthesis.punctuation == '(' => self Reading::IntoDelimited(left_parenthesis) if left_parenthesis.punctuation == '(' => self
.parse_parenthesized(handler) .parse_parenthesized(handler)
@ -661,14 +733,6 @@ impl Parser<'_> {
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 =>
@ -843,4 +907,88 @@ impl Parser<'_> {
} }
} }
} }
/// Expects the next [`Token`] to be an [`TemplateStringLiteral`], and returns it.
///
/// # Errors
/// If the next [`Token`] is not an [`TemplateStringLiteral`].
pub fn parse_template_string_literal(
&mut self,
handler: &impl Handler<base::Error>,
) -> ParseResult<TemplateStringLiteral> {
let starting_backtick = self.parse_punctuation('`', true, handler)?;
let mut parts = Vec::new();
loop {
match self.stop_at_significant() {
Reading::Atomic(Token::Punctuation(ending_backtick))
if ending_backtick.punctuation == '`' =>
{
self.forward();
// closing tick
return Ok(TemplateStringLiteral {
starting_backtick,
parts,
ending_backtick,
});
}
Reading::Atomic(Token::Punctuation(dollar)) if dollar.punctuation == '$' => {
self.forward();
let delimited_expression = self.step_into(
Delimiter::Parenthesis,
|parser| parser.parse_expression(handler),
handler,
)?;
parts.push(TemplateStringLiteralPart::Expression {
dollar,
open_brace: delimited_expression.open,
expression: Box::new(delimited_expression.tree?),
close_brace: delimited_expression.close,
});
}
Reading::Atomic(Token::TemplateStringText(text)) => {
self.forward();
parts.push(TemplateStringLiteralPart::Text(text));
}
unexpected => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: syntax::error::SyntaxKind::TemplateStringLiteralPart,
found: unexpected.into_token(),
});
handler.receive(Box::new(err.clone()));
return 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::Punctuation(punc)) if punc.punctuation == '`' => self
.parse_template_string_literal(handler)
.map(AnyStringLiteral::TemplateStringLiteral),
found => {
let err = Error::UnexpectedSyntax(UnexpectedSyntax {
expected: SyntaxKind::AnyStringLiteral,
found: found.into_token(),
});
handler.receive(Box::new(err.clone()));
Err(err)
}
}
}
} }

View File

@ -12,10 +12,10 @@ use crate::{
Handler, VoidHandler, Handler, VoidHandler,
}, },
lexical::{ lexical::{
token::{Identifier, MacroStringLiteral, Punctuation, StringLiteral, Token}, token::{Identifier, Punctuation, StringLiteral, Token},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::parser::Reading, syntax::{parser::Reading, syntax_tree::expression::TemplateStringLiteral},
}; };
use super::{ use super::{
@ -69,25 +69,25 @@ 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. /// Represents a syntax tree node that can be either a string literal or a template string literal.
/// ///
/// Syntax Synopsis: /// Syntax Synopsis:
/// ```ebnf /// ```ebnf
/// AnyStringLiteral: StringLiteral | MacroStringLiteral ; /// AnyStringLiteral: StringLiteral | TemplateStringLiteral ;
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
#[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, From)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, From)]
pub enum AnyStringLiteral { pub enum AnyStringLiteral {
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
MacroStringLiteral(MacroStringLiteral), TemplateStringLiteral(TemplateStringLiteral),
} }
impl SourceElement for AnyStringLiteral { impl SourceElement for AnyStringLiteral {
fn span(&self) -> Span { fn span(&self) -> Span {
match self { match self {
Self::StringLiteral(string_literal) => string_literal.span(), Self::StringLiteral(string_literal) => string_literal.span(),
Self::MacroStringLiteral(macro_string_literal) => macro_string_literal.span(), Self::TemplateStringLiteral(template_string_literal) => template_string_literal.span(),
} }
} }
} }

View File

@ -1010,7 +1010,6 @@ impl Parser<'_> {
/// ///
/// # Errors /// # Errors
/// - cannot parse variable declaration from current position /// - cannot parse variable declaration from current position
#[expect(clippy::too_many_lines)]
#[tracing::instrument(level = "trace", skip_all)] #[tracing::instrument(level = "trace", skip_all)]
pub fn parse_variable_declaration( pub fn parse_variable_declaration(
&mut self, &mut self,

View File

@ -1,8 +1,16 @@
//! Conversion functions for converting between tokens/ast-nodes and [`shulkerbox`] types //! Conversion functions for converting between tokens/ast-nodes and [`shulkerbox`] types
use std::{borrow::Cow, sync::Arc};
use shulkerbox::util::{MacroString as ExtMacroString, MacroStringPart as ExtMacroStringPart}; use shulkerbox::util::{MacroString as ExtMacroString, MacroStringPart as ExtMacroStringPart};
use crate::{lexical::token::MacroStringLiteral, syntax::syntax_tree::AnyStringLiteral}; use crate::{
base::{self, Handler},
semantic::error::UnexpectedExpression,
syntax::syntax_tree::expression::{TemplateStringLiteral, TemplateStringLiteralPart},
transpile::{Scope, TranspileError, TranspileResult},
util,
};
use super::util::{MacroString, MacroStringPart}; use super::util::{MacroString, MacroStringPart};
@ -26,26 +34,38 @@ impl From<MacroStringPart> for ExtMacroStringPart {
} }
} }
impl From<&AnyStringLiteral> for ExtMacroString { impl TemplateStringLiteral {
fn from(value: &AnyStringLiteral) -> Self { pub fn as_str(
Self::from(MacroString::from(value)) &self,
} scope: &Arc<Scope>,
} handler: &impl Handler<base::Error>,
) -> TranspileResult<Cow<'_, str>> {
let mut res = Cow::Borrowed("");
impl From<AnyStringLiteral> for ExtMacroString { for part in &self.parts {
fn from(value: AnyStringLiteral) -> Self { match part {
Self::from(&value) TemplateStringLiteralPart::Text(s) => {
} let s = util::unescape_template_string(s.span.str());
} if res.is_empty() {
res = s;
} else {
res.to_mut().push_str(&s);
}
}
TemplateStringLiteralPart::Expression { expression, .. } => {
let compiled = expression.comptime_eval(scope, handler)?;
let s = compiled.to_string_no_macro().ok_or_else(|| {
let err = TranspileError::UnexpectedExpression(UnexpectedExpression(
expression.clone(),
));
handler.receive(Box::new(err.clone()));
err
})?;
res.to_mut().push_str(&s);
}
}
}
impl From<&MacroStringLiteral> for ExtMacroString { Ok(res)
fn from(value: &MacroStringLiteral) -> Self {
Self::from(MacroString::from(value))
}
}
impl From<MacroStringLiteral> for ExtMacroString {
fn from(value: MacroStringLiteral) -> Self {
Self::from(&value)
} }
} }

View File

@ -20,7 +20,7 @@ use super::{
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
use crate::{ use crate::{
base::{self, source_file::SourceElement, Handler, VoidHandler}, base::{self, source_file::SourceElement, Handler, VoidHandler},
lexical::token::{Identifier, MacroStringLiteralPart, StringLiteral}, lexical::token::{Identifier, StringLiteral},
syntax::syntax_tree::expression::{ syntax::syntax_tree::expression::{
Binary, BinaryOperator, Expression, Indexed, MemberAccess, Parenthesized, PrefixOperator, Binary, BinaryOperator, Expression, Indexed, MemberAccess, Parenthesized, PrefixOperator,
Primary, Primary,
@ -337,7 +337,7 @@ impl Primary {
} }
} }
} }
Self::StringLiteral(_) | Self::MacroStringLiteral(_) => { Self::StringLiteral(_) | Self::TemplateStringLiteral(_) => {
matches!(r#type, ValueType::String | ValueType::Boolean) matches!(r#type, ValueType::String | ValueType::Boolean)
} }
} }
@ -412,17 +412,29 @@ impl Primary {
expression: lua.span(), expression: lua.span(),
}) })
.and_then(|val| val), .and_then(|val| val),
Self::MacroStringLiteral(macro_string_literal) => { Self::TemplateStringLiteral(template_string_literal) => {
if macro_string_literal use crate::syntax::syntax_tree::expression::TemplateStringLiteralPart;
if template_string_literal
.parts() .parts()
.iter() .iter()
.any(|part| matches!(part, MacroStringLiteralPart::MacroUsage { .. })) .any(|part| matches!(part, TemplateStringLiteralPart::Expression { .. }))
{ {
Ok(ComptimeValue::MacroString( template_string_literal
macro_string_literal.clone().into(), .to_macro_string(scope, handler)
)) .map(ComptimeValue::MacroString)
.map_err(|_| NotComptime {
expression: template_string_literal.span(),
})
} else { } else {
Ok(ComptimeValue::String(macro_string_literal.str_content())) Ok(ComptimeValue::String(
template_string_literal
.as_str(scope, handler)
.map_err(|_| NotComptime {
expression: template_string_literal.span(),
})?
.into_owned(),
))
} }
} }
} }
@ -446,7 +458,7 @@ impl Primary {
| Self::FunctionCall(_) | Self::FunctionCall(_)
| Self::Integer(_) | Self::Integer(_)
| Self::Lua(_) | Self::Lua(_)
| Self::MacroStringLiteral(_) | Self::TemplateStringLiteral(_)
| Self::MemberAccess(_) | Self::MemberAccess(_)
| Self::Prefix(_) => Err(NotComptime { | Self::Prefix(_) => Err(NotComptime {
expression: self.span(), expression: self.span(),
@ -476,7 +488,7 @@ impl Primary {
| Self::FunctionCall(_) | Self::FunctionCall(_)
| Self::Integer(_) | Self::Integer(_)
| Self::Lua(_) | Self::Lua(_)
| Self::MacroStringLiteral(_) | Self::TemplateStringLiteral(_)
| Self::MemberAccess(_) | Self::MemberAccess(_)
| Self::Prefix(_) => false, | Self::Prefix(_) => false,
} }
@ -1260,7 +1272,7 @@ impl Transpiler {
Err(err) Err(err)
} }
} }
Primary::StringLiteral(_) | Primary::MacroStringLiteral(_) => { Primary::StringLiteral(_) | Primary::TemplateStringLiteral(_) => {
if matches!( if matches!(
target, target,
DataLocation::Storage { DataLocation::Storage {
@ -1662,9 +1674,11 @@ impl Transpiler {
Vec::new(), Vec::new(),
ExtendedCondition::Runtime(Condition::Atom(s.str_content().to_string().into())), ExtendedCondition::Runtime(Condition::Atom(s.str_content().to_string().into())),
)), )),
Primary::MacroStringLiteral(macro_string) => Ok(( Primary::TemplateStringLiteral(template_string) => Ok((
Vec::new(), Vec::new(),
ExtendedCondition::Runtime(Condition::Atom(macro_string.into())), ExtendedCondition::Runtime(Condition::Atom(
template_string.to_macro_string(scope, handler)?.into(),
)),
)), )),
Primary::FunctionCall(func) => { Primary::FunctionCall(func) => {
if func if func

View File

@ -448,8 +448,8 @@ impl Transpiler {
Expression::Primary(Primary::StringLiteral(string)) => { Expression::Primary(Primary::StringLiteral(string)) => {
Ok(Parameter::Static(string.str_content().to_string().into())) Ok(Parameter::Static(string.str_content().to_string().into()))
} }
Expression::Primary(Primary::MacroStringLiteral(literal)) => { Expression::Primary(Primary::TemplateStringLiteral(literal)) => {
Ok(Parameter::Static(literal.into())) Ok(Parameter::Static(literal.to_macro_string(scope, handler)?))
} }
Expression::Primary(primary @ Primary::Identifier(ident)) => { Expression::Primary(primary @ Primary::Identifier(ident)) => {
let var = let var =

View File

@ -12,11 +12,13 @@ use serde_json::{json, Value as JsonValue};
use crate::{ use crate::{
base::{source_file::SourceElement as _, VoidHandler}, base::{source_file::SourceElement as _, VoidHandler},
lexical::token::{Identifier, MacroStringLiteralPart}, lexical::token::Identifier,
semantic::error::{InvalidFunctionArguments, UnexpectedExpression}, semantic::error::{InvalidFunctionArguments, UnexpectedExpression},
syntax::syntax_tree::expression::{Expression, FunctionCall, Primary}, syntax::syntax_tree::expression::{
Expression, FunctionCall, Primary, TemplateStringLiteralPart,
},
transpile::{ transpile::{
error::{IllegalIndexing, IllegalIndexingReason, UnknownIdentifier}, error::{IllegalIndexing, IllegalIndexingReason, NotComptime, UnknownIdentifier},
expression::{ComptimeValue, DataLocation, ExpectedType, StorageType}, expression::{ComptimeValue, DataLocation, ExpectedType, StorageType},
util::MacroString, util::MacroString,
TranspileError, TranspileError,
@ -95,7 +97,6 @@ fn print_function(
) -> TranspileResult<Vec<Command>> { ) -> TranspileResult<Vec<Command>> {
const PARAM_COLOR: &str = "gray"; const PARAM_COLOR: &str = "gray";
#[expect(clippy::option_if_let_else)]
fn get_identifier_part( fn get_identifier_part(
ident: &Identifier, ident: &Identifier,
transpiler: &mut Transpiler, transpiler: &mut Transpiler,
@ -131,6 +132,158 @@ fn print_function(
Ok((false, cmd, value)) Ok((false, cmd, value))
} }
VariableData::ComptimeValue { value, .. } => {
let value = {
let guard = value.read().map_err(|_| {
TranspileError::NotComptime(NotComptime {
expression: ident.span(),
})
})?;
guard.as_ref().map_or_else(
|| "null".into(),
super::expression::ComptimeValue::to_macro_string,
)
};
Ok((
value.contains_macros(),
None,
json!({"text": value.to_string(), "color": PARAM_COLOR}),
))
}
_ => Err(TranspileError::UnexpectedExpression(UnexpectedExpression(
Box::new(Expression::Primary(Primary::Identifier(ident.to_owned()))),
))),
}
} else {
Err(TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: ident.span(),
}))
}
}
fn get_indexed_part(
ident: &Identifier,
index: &Expression,
transpiler: &mut Transpiler,
scope: &Arc<Scope>,
) -> TranspileResult<(bool, Option<Command>, JsonValue)> {
if let Some(var) = scope.get_variable(ident.span.str()).as_deref() {
match var {
VariableData::Scoreboard { objective } => {
let Ok(ComptimeValue::String(target)) =
index.comptime_eval(scope, &VoidHandler)
else {
return Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
expression: index.span(),
}));
};
let (cmd, value) = get_data_location(
&DataLocation::ScoreboardValue {
objective: objective.to_string(),
target,
},
transpiler,
);
Ok((false, cmd, value))
}
VariableData::ScoreboardArray { objective, targets } => {
let Ok(ComptimeValue::Integer(idx)) = index.comptime_eval(scope, &VoidHandler)
else {
return Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: index.span(),
}));
};
#[expect(clippy::option_if_let_else)]
if let Some(target) = usize::try_from(idx)
.ok()
.and_then(|index| targets.get(index))
{
let (cmd, value) = get_data_location(
&DataLocation::ScoreboardValue {
objective: objective.to_string(),
target: target.to_string(),
},
transpiler,
);
Ok((false, cmd, value))
} else {
Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::IndexOutOfBounds {
index: usize::try_from(idx).unwrap_or(usize::MAX),
length: targets.len(),
},
expression: index.span(),
}))
}
}
VariableData::BooleanStorageArray {
storage_name,
paths,
} => {
let Ok(ComptimeValue::Integer(idx)) = index.comptime_eval(scope, &VoidHandler)
else {
return Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: index.span(),
}));
};
#[expect(clippy::option_if_let_else)]
if let Some(path) = usize::try_from(idx).ok().and_then(|index| paths.get(index))
{
let (cmd, value) = get_data_location(
&DataLocation::Storage {
storage_name: storage_name.to_string(),
path: path.to_string(),
r#type: StorageType::Boolean,
},
transpiler,
);
Ok((false, cmd, value))
} else {
Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::IndexOutOfBounds {
index: usize::try_from(idx).unwrap_or(usize::MAX),
length: paths.len(),
},
expression: index.span(),
}))
}
}
VariableData::Tag { tag_name } => {
let Ok(ComptimeValue::String(entity)) =
index.comptime_eval(scope, &VoidHandler)
else {
return Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
expression: index.span(),
}));
};
let (cmd, value) = get_data_location(
&DataLocation::Tag {
tag_name: tag_name.clone(),
entity,
},
transpiler,
);
Ok((false, cmd, value))
}
_ => Err(TranspileError::UnexpectedExpression(UnexpectedExpression( _ => Err(TranspileError::UnexpectedExpression(UnexpectedExpression(
Box::new(Expression::Primary(Primary::Identifier(ident.to_owned()))), Box::new(Expression::Primary(Primary::Identifier(ident.to_owned()))),
))), ))),
@ -192,7 +345,7 @@ fn print_function(
("@a".into(), first) ("@a".into(), first)
}; };
let mut contains_macro = matches!(target, MacroString::MacroString(_)); let mut contains_macro = target.contains_macros();
let (mut cmds, parts) = match message_expression { let (mut cmds, parts) = match message_expression {
Expression::Primary(primary) => match primary { Expression::Primary(primary) => match primary {
@ -343,20 +496,43 @@ fn print_function(
reason: IllegalIndexingReason::NotIdentifier, reason: IllegalIndexingReason::NotIdentifier,
})), })),
}, },
Primary::MacroStringLiteral(macro_string) => { Primary::TemplateStringLiteral(template_string) => {
let mut cmds = Vec::new(); let mut cmds = Vec::new();
let mut parts = Vec::new(); let mut parts = Vec::new();
for part in macro_string.parts() { for part in template_string.parts() {
match part { match part {
MacroStringLiteralPart::Text(text) => { TemplateStringLiteralPart::Text(text) => {
parts.push(JsonValue::String(text.str().to_string())); parts.push(JsonValue::String(text.span.str().to_string()));
} }
MacroStringLiteralPart::MacroUsage { identifier, .. } => { TemplateStringLiteralPart::Expression { expression, .. } => {
let (cur_contains_macro, cur_cmds, part) = match expression.as_ref() {
get_identifier_part(identifier, transpiler, scope)?; Expression::Primary(Primary::Identifier(identifier)) => {
contains_macro |= cur_contains_macro; let (cur_contains_macro, cur_cmds, part) =
cmds.extend(cur_cmds); get_identifier_part(identifier, transpiler, scope)?;
parts.push(part); contains_macro |= cur_contains_macro;
cmds.extend(cur_cmds);
parts.push(part);
}
Expression::Primary(Primary::Indexed(indexed)) => {
match indexed.object().as_ref() {
Primary::Identifier(ident) => {
let (cur_contains_macro, cur_cmds, part) =
get_indexed_part(
ident,
indexed.index(),
transpiler,
scope,
)?;
contains_macro |= cur_contains_macro;
cmds.extend(cur_cmds);
parts.push(part);
}
_ => todo!("other expression in indexed"),
}
}
_ => todo!("other expression in template string literal"),
}
} }
} }
} }

View File

@ -149,6 +149,8 @@ mod enabled {
table.set("version", crate::VERSION)?; table.set("version", crate::VERSION)?;
// TODO: add functions for requesting data/scoreboard locations
Ok(table) Ok(table)
} }
@ -323,26 +325,41 @@ mod enabled {
handler, handler,
), ),
Value::Boolean(boolean) => Ok(Some(ComptimeValue::Boolean(boolean))), Value::Boolean(boolean) => Ok(Some(ComptimeValue::Boolean(boolean))),
Value::Table(table) => match table.get::<Value>("value") { Value::Table(table) => {
Ok(Value::Nil) => { // TODO: allow to return arrays when comptime arrays are implemented
let err = TranspileError::LuaRuntimeError(LuaRuntimeError { match table.get::<Value>("value") {
code_block: self.span(), Ok(Value::Nil) => {
error_message: "return table must contain non-nil 'value'".to_string(), let err = TranspileError::LuaRuntimeError(LuaRuntimeError {
}); code_block: self.span(),
handler.receive(Box::new(err.clone())); error_message: "return table must contain non-nil 'value'"
Err(err) .to_string(),
} });
Ok(value) => { handler.receive(Box::new(err.clone()));
let value = match self.handle_lua_result(value, handler)? { Err(err)
Some(ComptimeValue::String(s)) => { }
let contains_macro = match table.get::<Value>("contains_macro") { Ok(value) => {
Ok(Value::Boolean(boolean)) => Ok(boolean), let value = match self.handle_lua_result(value, handler)? {
Ok(value) => { Some(ComptimeValue::String(s)) => {
if let Some(ComptimeValue::Boolean(boolean)) = let contains_macro = match table.get::<Value>("contains_macro")
self.handle_lua_result(value, handler)? {
{ Ok(Value::Boolean(boolean)) => Ok(boolean),
Ok(boolean) Ok(value) => {
} else { if let Some(ComptimeValue::Boolean(boolean)) =
self.handle_lua_result(value, handler)?
{
Ok(boolean)
} else {
let err = TranspileError::MismatchedTypes(
MismatchedTypes {
expression: self.span(),
expected_type: ExpectedType::Boolean,
},
);
handler.receive(Box::new(err.clone()));
Err(err)
}
}
_ => {
let err = let err =
TranspileError::MismatchedTypes(MismatchedTypes { TranspileError::MismatchedTypes(MismatchedTypes {
expression: self.span(), expression: self.span(),
@ -351,39 +368,29 @@ mod enabled {
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }
} }?;
_ => {
let err =
TranspileError::MismatchedTypes(MismatchedTypes {
expression: self.span(),
expected_type: ExpectedType::Boolean,
});
handler.receive(Box::new(err.clone()));
Err(err)
}
}?;
if contains_macro { if contains_macro {
Some(ComptimeValue::MacroString( Some(ComptimeValue::MacroString(
s.parse().expect("parsing cannot fail"), s.parse().expect("parsing cannot fail"),
)) ))
} else { } else {
Some(ComptimeValue::String(s)) Some(ComptimeValue::String(s))
}
} }
} value => value,
value => value, };
}; Ok(value)
Ok(value) }
Err(err) => {
let err = TranspileError::LuaRuntimeError(
LuaRuntimeError::from_lua_err(&err, self.span()),
);
handler.receive(Box::new(err.clone()));
Err(err)
}
} }
Err(err) => { }
let err = TranspileError::LuaRuntimeError(LuaRuntimeError::from_lua_err(
&err,
self.span(),
));
handler.receive(Box::new(err.clone()));
Err(err)
}
},
Value::Error(_) Value::Error(_)
| Value::Thread(_) | Value::Thread(_)
| Value::UserData(_) | Value::UserData(_)

View File

@ -69,7 +69,6 @@ impl Transpiler {
/// ///
/// # Errors /// # Errors
/// - [`TranspileError::MissingFunctionDeclaration`] If a called function is missing /// - [`TranspileError::MissingFunctionDeclaration`] If a called function is missing
#[expect(clippy::too_many_lines)]
#[tracing::instrument(level = "trace", skip_all)] #[tracing::instrument(level = "trace", skip_all)]
pub fn transpile( pub fn transpile(
mut self, mut self,
@ -645,7 +644,9 @@ impl Transpiler {
Primary::StringLiteral(string) => { Primary::StringLiteral(string) => {
Ok(vec![Command::Raw(string.str_content().to_string())]) Ok(vec![Command::Raw(string.str_content().to_string())])
} }
Primary::MacroStringLiteral(string) => Ok(vec![Command::UsesMacro(string.into())]), Primary::TemplateStringLiteral(string) => Ok(vec![Command::UsesMacro(
string.to_macro_string(scope, handler)?.into(),
)]),
Primary::Lua(code) => match code.eval_comptime(scope, handler)? { Primary::Lua(code) => match code.eval_comptime(scope, handler)? {
Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]), Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]),
Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]), Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]),
@ -987,37 +988,41 @@ impl Transpiler {
} }
} }
ExecuteBlockHead::As(r#as) => { ExecuteBlockHead::As(r#as) => {
let selector = r#as.as_selector(); let selector = r#as.as_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::As(selector.into(), Box::new(tail))) (pre_cmds, Execute::As(selector.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::At(at) => { ExecuteBlockHead::At(at) => {
let selector = at.at_selector(); let selector = at.at_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::At(selector.into(), Box::new(tail))) (pre_cmds, Execute::At(selector.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Align(align) => { ExecuteBlockHead::Align(align) => {
let align = align.align_selector(); let align = align.align_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Align(align.into(), Box::new(tail))) (pre_cmds, Execute::Align(align.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Anchored(anchored) => { ExecuteBlockHead::Anchored(anchored) => {
let anchor = anchored.anchored_selector(); let anchor = anchored
.anchored_selector()
.to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Anchored(anchor.into(), Box::new(tail))) (pre_cmds, Execute::Anchored(anchor.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::In(r#in) => { ExecuteBlockHead::In(r#in) => {
let dimension = r#in.in_selector(); let dimension = r#in.in_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::In(dimension.into(), Box::new(tail))) (pre_cmds, Execute::In(dimension.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Positioned(positioned) => { ExecuteBlockHead::Positioned(positioned) => {
let position = positioned.positioned_selector(); let position = positioned
.positioned_selector()
.to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
( (
pre_cmds, pre_cmds,
@ -1026,37 +1031,37 @@ impl Transpiler {
}) })
} }
ExecuteBlockHead::Rotated(rotated) => { ExecuteBlockHead::Rotated(rotated) => {
let rotation = rotated.rotated_selector(); let rotation = rotated.rotated_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Rotated(rotation.into(), Box::new(tail))) (pre_cmds, Execute::Rotated(rotation.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Facing(facing) => { ExecuteBlockHead::Facing(facing) => {
let facing = facing.facing_selector(); let facing = facing.facing_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Facing(facing.into(), Box::new(tail))) (pre_cmds, Execute::Facing(facing.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::AsAt(as_at) => { ExecuteBlockHead::AsAt(as_at) => {
let selector = as_at.asat_selector(); let selector = as_at.asat_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::AsAt(selector.into(), Box::new(tail))) (pre_cmds, Execute::AsAt(selector.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::On(on) => { ExecuteBlockHead::On(on) => {
let dimension = on.on_selector(); let dimension = on.on_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::On(dimension.into(), Box::new(tail))) (pre_cmds, Execute::On(dimension.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Store(store) => { ExecuteBlockHead::Store(store) => {
let store = store.store_selector(); let store = store.store_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Store(store.into(), Box::new(tail))) (pre_cmds, Execute::Store(store.into(), Box::new(tail)))
}) })
} }
ExecuteBlockHead::Summon(summon) => { ExecuteBlockHead::Summon(summon) => {
let entity = summon.summon_selector(); let entity = summon.summon_selector().to_macro_string(scope, handler)?;
tail.map(|(pre_cmds, tail)| { tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Summon(entity.into(), Box::new(tail))) (pre_cmds, Execute::Summon(entity.into(), Box::new(tail)))
}) })

View File

@ -1,12 +1,21 @@
//! Utility methods for transpiling //! Utility methods for transpiling
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr, sync::Arc};
use crate::{ use crate::{
lexical::token::{MacroStringLiteral, MacroStringLiteralPart}, base::{self, source_file::SourceElement as _, Handler},
syntax::syntax_tree::AnyStringLiteral, syntax::syntax_tree::{
expression::{Expression, Primary, TemplateStringLiteral, TemplateStringLiteralPart},
AnyStringLiteral,
},
transpile::{
error::{TranspileError, UnknownIdentifier},
Scope, TranspileResult, VariableData,
},
}; };
use super::expression::ComptimeValue;
/// String that can contain macros /// String that can contain macros
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -247,53 +256,90 @@ where
} }
} }
impl From<&AnyStringLiteral> for MacroString { impl AnyStringLiteral {
fn from(value: &AnyStringLiteral) -> Self { /// Convert the any string literal to a macro string, using the provided scope to resolve variables
match value { ///
AnyStringLiteral::StringLiteral(literal) => Self::from(literal.str_content().as_ref()), /// # Errors
AnyStringLiteral::MacroStringLiteral(literal) => Self::from(literal), /// - If an identifier in a template string is not found in the scope
pub fn to_macro_string(
&self,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<MacroString> {
match self {
Self::StringLiteral(literal) => Ok(MacroString::from(literal.str_content().as_ref())),
Self::TemplateStringLiteral(literal) => literal.to_macro_string(scope, handler),
} }
} }
} }
impl From<AnyStringLiteral> for MacroString { impl TemplateStringLiteral {
fn from(value: AnyStringLiteral) -> Self { /// Convert the template string literal to a macro string, using the provided scope to resolve variables
Self::from(&value) ///
} /// # Errors
} /// - If an identifier in a template string is not found in the scope
pub fn to_macro_string(
impl From<&MacroStringLiteral> for MacroString { &self,
fn from(value: &MacroStringLiteral) -> Self { scope: &Arc<Scope>,
if value handler: &impl Handler<base::Error>,
) -> TranspileResult<MacroString> {
if self
.parts() .parts()
.iter() .iter()
.any(|p| matches!(p, MacroStringLiteralPart::MacroUsage { .. })) .any(|p| matches!(p, TemplateStringLiteralPart::Expression { .. }))
{ {
Self::MacroString( let macro_string = MacroString::MacroString(
value self.parts()
.parts()
.iter() .iter()
.map(|part| match part { .map(|part| match part {
MacroStringLiteralPart::Text(span) => MacroStringPart::String( TemplateStringLiteralPart::Text(text) => Ok(MacroStringPart::String(
crate::util::unescape_macro_string(span.str()).to_string(), crate::util::unescape_template_string(text.span.str()).into_owned(),
), )),
MacroStringLiteralPart::MacroUsage { identifier, .. } => { TemplateStringLiteralPart::Expression { expression, .. } => {
MacroStringPart::MacroUsage( match expression.as_ref() {
crate::util::identifier_to_macro(identifier.span.str()).to_string(), Expression::Primary(Primary::Identifier(identifier)) =>
) {
#[expect(clippy::option_if_let_else)]
if let Some(var_data) =
scope.get_variable(identifier.span.str())
{
match var_data.as_ref() {
VariableData::MacroParameter { macro_name, .. } => Ok(
MacroStringPart::MacroUsage(macro_name.to_owned()),
),
VariableData::ComptimeValue { value, .. } => {
let value = value.read().unwrap().as_ref().map_or_else(
|| "null".into(),
ComptimeValue::to_macro_string,
);
match value.as_str() {
Ok(s) => Ok(MacroStringPart::String(s.into_owned())),
Err(_) => todo!("comptime value resulting in macro string with macros")
}
}
_ => todo!("other identifiers in template strings"),
}
} else {
let err =
TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: identifier.span(),
});
handler.receive(Box::new(err.clone()));
Err(err)
}
}
_ => todo!("other expressions in template strings"),
}
} }
}) })
.collect(), .collect::<TranspileResult<Vec<MacroStringPart>>>()?,
) );
} else {
Self::String(value.str_content())
}
}
}
impl From<MacroStringLiteral> for MacroString { Ok(macro_string)
fn from(value: MacroStringLiteral) -> Self { } else {
Self::from(&value) Ok(MacroString::String(self.as_str(scope, handler)?.into_owned()))
}
} }
} }

View File

@ -20,15 +20,16 @@ pub fn escape_str(s: &str) -> Cow<'_, str> {
} }
} }
/// Unescapes '\`', `\`, `\n`, `\r` and `\t` in a string. /// Unescapes '\`', `\`, `\n`, `\r` and `\t`, `\$` in a string.
#[must_use] #[must_use]
pub fn unescape_macro_string(s: &str) -> Cow<'_, str> { pub fn unescape_template_string(s: &str) -> Cow<'_, str> {
if s.contains('\\') || s.contains('`') { if s.contains('\\') || s.contains('`') {
Cow::Owned( Cow::Owned(
s.replace("\\n", "\n") s.replace("\\n", "\n")
.replace("\\r", "\r") .replace("\\r", "\r")
.replace("\\t", "\t") .replace("\\t", "\t")
.replace("\\`", "`") .replace("\\`", "`")
.replace("\\$", "$")
.replace("\\\\", "\\"), .replace("\\\\", "\\"),
) )
} else { } else {
@ -147,33 +148,33 @@ mod tests {
#[test] #[test]
fn test_unescape_macro_string() { fn test_unescape_macro_string() {
assert_eq!(unescape_macro_string("Hello, world!"), "Hello, world!"); assert_eq!(unescape_template_string("Hello, world!"), "Hello, world!");
assert_eq!( assert_eq!(
unescape_macro_string(r#"Hello, "world"!"#), unescape_template_string(r#"Hello, "world"!"#),
r#"Hello, "world"!"# r#"Hello, "world"!"#
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \world\!"), unescape_template_string(r"Hello, \world\!"),
r"Hello, \world\!" r"Hello, \world\!"
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \nworld\!"), unescape_template_string(r"Hello, \nworld\!"),
"Hello, \nworld\\!" "Hello, \nworld\\!"
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \rworld\!"), unescape_template_string(r"Hello, \rworld\!"),
"Hello, \rworld\\!" "Hello, \rworld\\!"
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \tworld\!"), unescape_template_string(r"Hello, \tworld\!"),
"Hello, \tworld\\!" "Hello, \tworld\\!"
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \`world\!"), unescape_template_string(r"Hello, \`world\!"),
r"Hello, `world\!" r"Hello, `world\!"
); );
assert_eq!( assert_eq!(
unescape_macro_string(r"Hello, \\world\!"), unescape_template_string(r"Hello, \\world\!"),
r"Hello, \world\!" r"Hello, \world\!"
); );
} }