add while command

This commit is contained in:
Moritz Hölting 2025-09-11 00:05:17 +02:00
parent d4689c696a
commit d9608a8f8c
9 changed files with 405 additions and 136 deletions

View File

@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- support for commands using macros
- support for registering scoreboards (automatic creation and deletion)
- "return" command with special handling in groups and conditionals
- while loop command
- `CommandCollection` trait for common operations on collections of commands (e.g. `Vec<Command>`)
### Changed
- use "return" command for conditionals instead of data storage when using supported pack format
- update latest datapack format to 61
- update latest datapack format to 81
### Removed

View File

@ -65,7 +65,7 @@ fn compile_using_data_storage(
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
let contains_macro = prefix_contains_macros || cond.contains_macro();
let contains_macro = prefix_contains_macros || cond.contains_macros();
let then_count = then.get_count(options);
let str_cond = cond.clone().compile(options, global_state, function_state);
@ -208,7 +208,7 @@ fn compile_since_20_format(
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
let contains_macros = prefix_contains_macros || cond.contains_macro();
let contains_macros = prefix_contains_macros || cond.contains_macros();
let then_count = then.get_count(options);
let str_cond = cond
@ -321,7 +321,7 @@ fn combine_conditions_commands_concat(
conditions
.into_iter()
.map(|cond| {
let prefix = if cond.contains_macro() {
let prefix = if cond.contains_macros() {
Command::UsesMacro(cond + " ")
} else {
Command::Raw(cond.compile() + " ")
@ -474,6 +474,15 @@ impl Condition {
}
}
#[must_use]
pub fn get_count(&self) -> usize {
match self.normalize() {
Self::Atom(_) | Self::Not(_) => 1,
Self::Or(a, b) => a.get_count() + b.get_count(),
Self::And(a, b) => a.get_count() * b.get_count(),
}
}
/// Convert the condition into a [`MacroString`].
///
/// Will fail if the condition contains an `Or` or double nested `Not` variant. Use `compile` instead.
@ -527,11 +536,11 @@ impl Condition {
/// Check whether the condition contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
pub fn contains_macros(&self) -> bool {
match self {
Self::Atom(s) => s.contains_macro(),
Self::Not(n) => n.contains_macro(),
Self::And(a, b) | Self::Or(a, b) => a.contains_macro() || b.contains_macro(),
Self::Atom(s) => s.contains_macros(),
Self::Not(n) => n.contains_macros(),
Self::And(a, b) | Self::Or(a, b) => a.contains_macros() || b.contains_macros(),
}
}

View File

@ -91,7 +91,7 @@ impl Execute {
arg = arg.compile()
),
require_grouping,
prefix_contains_macros || arg.contains_macro(),
prefix_contains_macros || arg.contains_macros(),
options,
global_state,
function_state,
@ -102,7 +102,7 @@ impl Execute {
selector = selector.compile()
),
require_grouping,
prefix_contains_macros || selector.contains_macro(),
prefix_contains_macros || selector.contains_macros(),
options,
global_state,
function_state,
@ -124,7 +124,7 @@ impl Execute {
arg = arg.compile()
),
true,
prefix_contains_macros || arg.contains_macro(),
prefix_contains_macros || arg.contains_macros(),
options,
global_state,
function_state,
@ -247,7 +247,7 @@ impl Execute {
/// Check whether the execute command contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
pub fn contains_macros(&self) -> bool {
match self {
Self::Facing(s, next)
| Self::Store(s, next)
@ -260,14 +260,14 @@ impl Execute {
| Self::Align(s, next)
| Self::Anchored(s, next)
| Self::Summon(s, next)
| Self::On(s, next) => s.contains_macro() || next.contains_macro(),
| Self::On(s, next) => s.contains_macros() || next.contains_macros(),
Self::If(cond, then, el) => {
cond.contains_macro()
|| then.contains_macro()
|| el.as_deref().is_some_and(Self::contains_macro)
cond.contains_macros()
|| then.contains_macros()
|| el.as_deref().is_some_and(Self::contains_macros)
}
Self::Run(cmd) => cmd.contains_macro(),
Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro),
Self::Run(cmd) => cmd.contains_macros(),
Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macros),
}
}

View File

@ -17,7 +17,7 @@ use crate::{
prelude::Datapack,
util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
MacroString,
CommandCollection, MacroString, MacroStringPart,
},
};
@ -39,6 +39,8 @@ pub enum Command {
Comment(String),
/// Return value
Return(ReturnCommand),
/// While loop
While(While),
/// Command that is a concatenation of two commands
Concat(Box<Command>, Box<Command>),
@ -70,6 +72,8 @@ impl Command {
vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)]
}
Self::Return(return_cmd) => return_cmd.compile(options, global_state, function_state),
Self::While(while_cmd) => while_cmd.compile(options, global_state, function_state),
Self::Concat(a, b) => {
let a = a.compile(options, global_state, function_state);
let b = b.compile(options, global_state, function_state);
@ -104,6 +108,8 @@ impl Command {
Self::Execute(ex) => ex.get_count(options),
Self::Group(group) => group.get_count(options),
Self::Return(_) => 1,
Self::While(while_cmd) => while_cmd.get_count(options),
Self::Concat(a, b) => a.get_count(options) + b.get_count(options) - 1,
}
}
@ -112,7 +118,7 @@ impl Command {
#[must_use]
pub fn validate(&self, pack_formats: &RangeInclusive<u8>) -> bool {
let command_valid = match self {
Self::Comment(_) | Self::Debug(_) | Self::Group(_) => true,
Self::Comment(_) | Self::Debug(_) | Self::Group(_) | Self::While(_) => true,
Self::Raw(cmd) => validate_raw_cmd(cmd, pack_formats),
Self::UsesMacro(cmd) => validate_raw_cmd(&cmd.compile(), pack_formats),
Self::Execute(ex) => ex.validate(pack_formats),
@ -122,10 +128,11 @@ impl Command {
pack_formats.start() >= &16 && cmd.validate(pack_formats)
}
},
Self::Concat(a, b) => a.validate(pack_formats) && b.validate(pack_formats),
};
if pack_formats.start() < &16 {
command_valid && !self.contains_macro()
command_valid && !self.contains_macros()
} else {
command_valid
}
@ -133,17 +140,19 @@ impl Command {
/// Check whether the command contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
pub fn contains_macros(&self) -> bool {
match self {
Self::Raw(_) | Self::Comment(_) => false,
Self::UsesMacro(s) | Self::Debug(s) => s.contains_macro(),
Self::Group(group) => group.contains_macro(),
Self::Execute(ex) => ex.contains_macro(),
Self::UsesMacro(s) | Self::Debug(s) => s.contains_macros(),
Self::Group(group) => group.contains_macros(),
Self::Execute(ex) => ex.contains_macros(),
Self::Return(ret) => match ret {
ReturnCommand::Value(value) => value.contains_macro(),
ReturnCommand::Command(cmd) => cmd.contains_macro(),
ReturnCommand::Value(value) => value.contains_macros(),
ReturnCommand::Command(cmd) => cmd.contains_macros(),
},
Self::Concat(a, b) => a.contains_macro() || b.contains_macro(),
Self::While(while_cmd) => while_cmd.contains_macros(),
Self::Concat(a, b) => a.contains_macros() || b.contains_macros(),
}
}
@ -159,6 +168,8 @@ impl Command {
ReturnCommand::Value(value) => value.get_macros(),
ReturnCommand::Command(cmd) => cmd.get_macros(),
},
Self::While(while_cmd) => while_cmd.get_macros(),
Self::Concat(a, b) => {
let mut macros = a.get_macros();
macros.extend(b.get_macros());
@ -172,12 +183,17 @@ impl Command {
pub fn forbid_prefix(&self) -> bool {
match self {
Self::Comment(_) => true,
Self::Raw(_) | Self::Debug(_) | Self::Execute(_) | Self::UsesMacro(_) => false,
Self::Raw(_)
| Self::Debug(_)
| Self::Execute(_)
| Self::UsesMacro(_)
| Self::While(_) => false,
Self::Group(group) => group.forbid_prefix(),
Self::Return(ret) => match ret {
ReturnCommand::Value(_) => false,
ReturnCommand::Command(cmd) => cmd.forbid_prefix(),
},
Self::Concat(a, _) => a.forbid_prefix(),
}
}
@ -188,11 +204,13 @@ impl Command {
match self {
Self::Comment(_) | Self::Debug(_) => false,
Self::Return(_) => true,
Self::Concat(a, b) => a.contains_return() || b.contains_return(),
Self::Execute(exec) => exec.contains_return(),
Self::Raw(cmd) => cmd.starts_with("return "),
Self::UsesMacro(m) => m.compile().starts_with("return "),
Self::Group(g) => g.contains_return(),
Self::While(w) => w.contains_return(),
Self::Concat(a, b) => a.contains_return() || b.contains_return(),
}
}
}
@ -213,6 +231,24 @@ impl From<&mut Function> for Command {
}
}
impl From<ReturnCommand> for Command {
fn from(value: ReturnCommand) -> Self {
Self::Return(value)
}
}
impl From<Group> for Command {
fn from(value: Group) -> Self {
Self::Group(value)
}
}
impl From<While> for Command {
fn from(value: While) -> Self {
Self::While(value)
}
}
/// Represents a group of commands to be executed in sequence.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
@ -279,25 +315,40 @@ impl Group {
self.commands[0].compile(options, global_state, function_state)
}
_ => {
let pass_macros = self.contains_macro();
let contains_return = self.contains_return();
let commands = &self.commands;
let block_pass_macros = self.block_pass_macros.as_ref();
let macro_data_storage_name = self.data_storage_name.as_deref();
// calculate a hashed path for the function in the `sb` subfolder
let function_path = Self::generate_function_path(function_state);
let function_path = generate_group_function_path(function_state);
let namespace = function_state.namespace();
// create a new function with the commands
let mut function = Function::new(namespace, &function_path);
function
.get_commands_mut()
.extend(self.commands.iter().cloned());
function_state.add_function(&function_path, function);
let contained_macros = {
let mut macros = HashSet::new();
for cmd in commands {
macros.extend(cmd.get_macros());
}
macros
};
let (prepare_data_storage, function_invocation) = get_group_invocation_commands(
&function_path,
namespace,
commands,
&contained_macros,
block_pass_macros,
macro_data_storage_name,
);
let mut function_invocation = format!("function {namespace}:{function_path}");
create_group_function(&function_path, commands, function_state);
let contains_return = commands.contains_return();
let additional_return_cmds = if contains_return {
let full_path = format!("{namespace}:{function_path}");
let full_path = format!(
"{namespace}:{function_path}",
namespace = function_state.namespace()
);
let return_data_path = md5::hash(&full_path).to_hex_lowercase();
let pre_cmds = Command::Raw(format!(
@ -312,23 +363,23 @@ impl Group {
);
let post_cmd_store = global_state
.read()
.unwrap()
.functions_with_special_return
.get(&format!(
"{}:{}",
function_state.namespace(),
function_state.path()
))
.cloned().map(|parent_return_data_path| {
Command::Execute(Execute::If(
post_condition.clone(),
Box::new(Execute::Run(Box::new(Command::Raw(format!(
"data modify storage shulkerbox:return {parent_return_data_path} set from storage shulkerbox:return {return_data_path}"
))))),
None,
))
});
.read()
.unwrap()
.functions_with_special_return
.get(&format!(
"{}:{}",
function_state.namespace(),
function_state.path()
))
.cloned().map(|parent_return_data_path| {
Command::Execute(Execute::If(
post_condition.clone(),
Box::new(Execute::Run(Box::new(Command::Raw(format!(
"data modify storage shulkerbox:return {parent_return_data_path} set from storage shulkerbox:return {return_data_path}"
))))),
None,
))
});
let post_cmd_return = Command::Execute(Execute::If(
post_condition,
@ -356,72 +407,26 @@ impl Group {
None
};
let prepare_data_storage = if pass_macros {
let contained_macros = self.get_macros();
let not_all_macros_blocked = self
.block_pass_macros
.as_ref()
.is_none_or(|b| contained_macros.iter().any(|&m| !b.contains(m)));
if !contained_macros.is_empty()
&& (self.data_storage_name.is_some() || not_all_macros_blocked)
{
use std::fmt::Write as _;
// WARNING: this seems to be the only way to pass macros to the function called.
// Because everything is passed as a string, it looses one "level" of escaping per pass.
let macros_block = self
.get_macros()
.into_iter()
.filter(|&m| {
self.block_pass_macros
.as_ref()
.is_none_or(|b| !b.contains(m))
})
.map(|m| format!(r#"{m}:"$({m})""#))
.collect::<Vec<_>>()
.join(",");
if let Some(data_storage_name) = self.data_storage_name.as_deref() {
let _ =
write!(function_invocation, " with storage {data_storage_name}");
not_all_macros_blocked.then(|| {
CompiledCommand::new(format!(
"data merge storage {data_storage_name} {{{macros_block}}}"
))
.with_contains_macros(true)
})
} else {
let _ = write!(function_invocation, " {{{macros_block}}}");
None
}
} else {
None
}
} else {
None
};
if let Some((mut pre_cmds, post_cmds)) = additional_return_cmds {
if let Some(prepare_datastorage_cmd) = prepare_data_storage {
pre_cmds.push(prepare_datastorage_cmd);
pre_cmds.extend(prepare_datastorage_cmd.compile(
options,
global_state,
function_state,
));
}
pre_cmds.push(
CompiledCommand::new(function_invocation)
.with_contains_macros(pass_macros && self.data_storage_name.is_none()),
);
pre_cmds.extend(function_invocation.compile(
options,
global_state,
function_state,
));
pre_cmds.extend(post_cmds);
pre_cmds
} else {
prepare_data_storage
.into_iter()
.chain(std::iter::once(
CompiledCommand::new(function_invocation).with_contains_macros(
pass_macros && self.data_storage_name.is_none(),
),
))
.flat_map(|prep| prep.compile(options, global_state, function_state))
.chain(function_invocation.compile(options, global_state, function_state))
.collect()
}
}
@ -460,27 +465,14 @@ impl Group {
/// Check whether the group contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
self.commands.iter().any(Command::contains_macro)
pub fn contains_macros(&self) -> bool {
self.commands.contains_macros()
}
/// Check whether the group contains a return command.
#[must_use]
pub fn contains_return(&self) -> bool {
self.commands.iter().any(Command::contains_return)
}
/// Generate a unique function path based on the function state.
#[must_use]
fn generate_function_path(function_state: &FunctionCompilerState) -> String {
let uid = function_state.request_uid();
let function_path = function_state.path();
let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path);
let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string();
let hash = md5::hash(pre_hash_path).to_hex_lowercase();
"sb/".to_string() + function_path + "/" + &hash[..16]
self.commands.contains_return()
}
/// Returns the names of the macros used
@ -512,7 +504,7 @@ impl Group {
0 if !self.always_create_function => 0,
1 if !self.always_create_function => 1,
_ => {
let pass_macros = self.contains_macro();
let pass_macros = self.contains_macros();
let contains_return = self.contains_return();
let additional_return_cmds = if contains_return {
@ -575,6 +567,101 @@ impl Hash for Group {
}
}
fn get_group_invocation_commands(
function_path: &str,
namespace: &str,
commands: &[Command],
contained_macros: &HashSet<&str>,
block_pass_macros: Option<&HashSet<String>>,
macro_data_storage_name: Option<&str>,
) -> (Option<Command>, Command) {
use std::fmt::Write as _;
let mut function_invocation = format!("function {namespace}:{function_path}");
if commands.contains_macros() {
// WARNING: this seems to be the only way to pass macros to the function called.
// Because everything is passed as a string, it looses one "level" of escaping per pass.
let mut macros_parts = contained_macros
.iter()
.filter(|&&m| block_pass_macros.is_none_or(|b| !b.contains(m)))
.flat_map(|&m| {
vec![
MacroStringPart::String(format!(r#"{m}:""#)),
MacroStringPart::MacroUsage(m.to_string()),
MacroStringPart::String(r#"""#.to_string()),
]
})
.fold(Vec::new(), |mut acc, part| {
if !acc.is_empty() {
acc.push(MacroStringPart::String(",".to_string()));
}
acc.push(part);
acc
});
if let Some(data_storage_name) = macro_data_storage_name {
let _ = write!(function_invocation, " with storage {data_storage_name}");
let not_all_macros_blocked =
block_pass_macros.is_none_or(|b| contained_macros.iter().any(|&m| !b.contains(m)));
if not_all_macros_blocked {
macros_parts.insert(
0,
MacroStringPart::String(format!("data merge storage {data_storage_name} {{")),
);
macros_parts.push(MacroStringPart::String("}".to_string()));
let macro_string = MacroString::MacroString(macros_parts).normalize();
(
Some(Command::UsesMacro(macro_string)),
Command::Raw(function_invocation),
)
} else {
(None, Command::Raw(function_invocation))
}
} else {
let _ = write!(function_invocation, " {{");
macros_parts.insert(0, MacroStringPart::String(function_invocation));
macros_parts.push(MacroStringPart::String("}".to_string()));
let macro_string = MacroString::MacroString(macros_parts).normalize();
(None, Command::UsesMacro(macro_string))
}
} else {
(None, Command::Raw(function_invocation))
}
}
fn create_group_function(
function_path: &str,
commands: &[Command],
function_state: &FunctionCompilerState,
) {
let namespace = function_state.namespace();
// create a new function with the commands
let mut function = Function::new(namespace, function_path);
function.get_commands_mut().extend(commands.iter().cloned());
function_state.add_function(function_path, function);
}
#[must_use]
fn generate_group_function_path(function_state: &FunctionCompilerState) -> String {
let uid = function_state.request_uid();
let function_path = function_state.path();
let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path);
let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string();
let hash = md5::hash(pre_hash_path).to_hex_lowercase();
"sb/".to_string() + function_path + "/" + &hash[..16]
}
/// Represents a command that returns a value.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -658,6 +745,104 @@ impl ReturnCommand {
}
}
/// Loops the commands while a condition is true.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct While {
condition: Condition,
commands: Vec<Command>,
}
impl While {
pub fn new(condition: impl Into<Condition>, commands: Vec<Command>) -> Self {
Self {
condition: condition.into(),
commands,
}
}
pub fn compile(
&self,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
// calculate a hashed path for the function in the `sb` subfolder
let function_path = generate_group_function_path(function_state);
let namespace = function_state.namespace();
let contained_macros = {
let mut macros = HashSet::new();
for cmd in &self.commands {
macros.extend(cmd.get_macros());
}
macros
};
let (prepare_data_storage, function_invocation) = get_group_invocation_commands(
&function_path,
namespace,
&self.commands,
&contained_macros,
None,
None,
);
let execute_tail = if let Some(prepare_datastorage_cmd) = prepare_data_storage {
Execute::Runs(vec![prepare_datastorage_cmd, function_invocation])
} else {
Execute::Run(Box::new(function_invocation))
};
let conditional_run_cmd = Command::Execute(Execute::If(
self.condition.clone(),
Box::new(execute_tail),
None,
));
let mut commands = self.commands.clone();
commands.push(conditional_run_cmd.clone());
create_group_function(&function_path, &commands, function_state);
let contains_return = commands.contains_return();
if contains_return {
todo!("While loops with return commands are not yet supported");
}
conditional_run_cmd.compile(options, global_state, function_state)
}
#[must_use]
pub fn get_count(&self, options: &CompileOptions) -> usize {
Execute::If(
self.condition.clone(),
Box::new(Execute::Run(Box::new(Command::Raw(String::new())))),
None,
)
.get_count(options)
}
#[must_use]
pub fn contains_macros(&self) -> bool {
self.condition.contains_macros() || self.commands.contains_macros()
}
#[must_use]
pub fn get_macros(&self) -> HashSet<&str> {
let mut macros = self.condition.get_macros();
for cmd in &self.commands {
macros.extend(cmd.get_macros());
}
macros
}
#[must_use]
pub fn contains_return(&self) -> bool {
self.commands.contains_return()
}
}
#[allow(clippy::too_many_lines)]
fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> bool {
static CMD_FORMATS: LazyLock<HashMap<&str, RangeInclusive<u8>>> = LazyLock::new(|| {

View File

@ -65,7 +65,7 @@ impl Function {
.flat_map(|c| {
let cmds = c.compile(options, global_state, function_state);
if c.contains_macro() {
if c.contains_macros() {
cmds.into_iter()
.map(|c| {
if c.contains_macros() {

View File

@ -4,7 +4,7 @@ mod command;
mod function;
mod namespace;
pub mod tag;
pub use command::{Command, Condition, Execute, Group, ReturnCommand};
pub use command::{Command, Condition, Execute, Group, ReturnCommand, While};
pub use function::Function;
pub use namespace::Namespace;

View File

@ -0,0 +1,35 @@
use std::collections::HashSet;
use crate::prelude::Command;
/// A trait for collections that can hold `Command` items.
pub trait CommandCollection {
/// Returns an iterator over the commands in the collection.
fn commands(&self) -> impl Iterator<Item = &Command>;
/// Checks if any command in the collection contains macros.
fn contains_macros(&self) -> bool {
self.commands().any(Command::contains_macros)
}
/// Returns a set of all macro names used in the commands of the collection.
fn get_macros(&self) -> HashSet<&str> {
self.commands()
.flat_map(Command::get_macros)
.collect::<HashSet<&str>>()
}
/// Checks if any command in the collection is a return command.
fn contains_return(&self) -> bool {
self.commands().any(Command::contains_return)
}
}
impl<C> CommandCollection for C
where
C: AsRef<[Command]>,
{
fn commands(&self) -> impl Iterator<Item = &Command> {
self.as_ref().iter()
}
}

View File

@ -19,7 +19,7 @@ pub enum MacroStringPart {
impl MacroString {
/// Returns whether the [`MacroString`] contains any macro usages
#[must_use]
pub fn contains_macro(&self) -> bool {
pub fn contains_macros(&self) -> bool {
match self {
Self::String(_) => false,
Self::MacroString(parts) => !parts
@ -47,6 +47,40 @@ impl MacroString {
}
}
#[must_use]
pub fn normalize(self) -> Self {
match self {
Self::String(_) => self,
Self::MacroString(parts) => {
let mut normalized_parts = Vec::new();
for part in parts {
match part {
MacroStringPart::String(s) => {
if let Some(MacroStringPart::String(last)) = normalized_parts.last_mut()
{
last.push_str(&s);
} else if !s.is_empty() {
normalized_parts.push(MacroStringPart::String(s));
}
}
MacroStringPart::MacroUsage(m) => {
normalized_parts.push(MacroStringPart::MacroUsage(m));
}
}
}
if normalized_parts.len() == 1 {
if let MacroStringPart::String(s) = &normalized_parts[0] {
return Self::String(s.clone());
}
}
Self::MacroString(normalized_parts)
}
}
}
/// Returns the amount of lines the string has
#[must_use]
pub fn line_count(&self) -> usize {

View File

@ -1,5 +1,6 @@
//! Utility functions for the Shulkerbox project.
mod command_collection;
pub mod compile;
mod extendable_queue;
mod macro_string;
@ -10,3 +11,6 @@ pub use extendable_queue::ExtendableQueue;
#[doc(inline)]
pub use macro_string::{MacroString, MacroStringPart};
#[doc(inline)]
pub use command_collection::CommandCollection;