xmacro_lib 0.5.1

macro engine for producing multiple expansions
Documentation
#![allow(unreachable_code)]

use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::atomic::AtomicU32;

#[allow(clippy::wildcard_imports)]
use unsynn::*;

#[allow(clippy::wildcard_imports)]
use crate::xtoken::*;

// Inner scope representation, we define this as a separate struct, because the root scope is
// not surrounded by `${ }`.
pub struct XMacro<'a> {
    // the parsed content
    pub xtokens: Vec<XToken>,
    // Definitions are parsed
    pub definitions: Definitions<'a>,
}

impl Parser for XMacro<'_> {
    #[allow(clippy::too_many_lines)]
    fn parser(input: &mut TokenIter) -> Result<Self> {
        let mut scope_inner = Self {
            xtokens: Vec::new(),
            definitions: Definitions::new(None),
        };

        scope_inner.xtokens = parse_scope_content(input, &mut scope_inner, false)?;
        Ok(scope_inner)
    }
}

pub struct XMacroItem<'a>(pub XMacro<'a>);

impl Parser for XMacroItem<'_> {
    #[allow(clippy::too_many_lines)]
    fn parser(input: &mut TokenIter) -> Result<Self> {
        let mut scope_inner = Self(XMacro {
            xtokens: Vec::new(),
            definitions: Definitions::new(None),
        });

        scope_inner.0.xtokens = parse_scope_content(input, &mut scope_inner.0, true)?;
        Ok(scope_inner)
    }
}

// We need to recurse into groups, thus we need a function
#[allow(clippy::too_many_lines)]
#[allow(clippy::items_after_statements)]
fn parse_scope_content(
    input: &mut TokenIter,
    scope_inner: &mut XMacro,
    break_at_item: bool,
) -> Result<Vec<XToken>> {
    let mut xtokens = Vec::new();
    loop {
        match XToken::parse(input) {
            //recurse groups
            Ok(XToken::Group(group)) => {
                xtokens.push(XToken::XGroup(GroupContaining {
                    delimiter: group.delimiter(),
                    content: parse_scope_content(
                        &mut group.stream().into_token_iter(),
                        scope_inner,
                        false,
                    )?,
                }));
                if break_at_item && group.delimiter() == Delimiter::Brace {
                    break;
                }
            }

            // scopes are parsed and expanded in place
            Ok(XToken::Scope(_, braced)) => {
                let mut child_inner = XMacro {
                    xtokens: Vec::new(),
                    definitions: Definitions::new(Some(&scope_inner.definitions)),
                };

                child_inner.xtokens = parse_scope_content(
                    &mut braced.0.stream().to_token_iter(),
                    &mut child_inner,
                    false,
                )?;

                // expand the child scope
                let mut output = TokenStream::new();
                for token in child_inner.expand() {
                    token.to_tokens(&mut output);
                }
                xtokens.push(XToken::TokenStream(output));
            }

            // register table definitions, will stay hidden, turn expansion mode to rows
            Ok(XToken::Definition(_, def)) => {
                let mut start_index = scope_inner.definitions.definitions.len();

                // find the inplace definitions
                let mut inplace_defs: Vec<bool> = def
                    .content
                    .names
                    .iter()
                    .map(|n| n.inplace.is_some())
                    .collect();
                // unnamed is always inplace
                if inplace_defs.is_empty() {
                    inplace_defs.push(true);
                }

                scope_inner
                    .definitions
                    .extend(def.content, input.counter())?;

                // expand the inplace definitions
                for inplace in inplace_defs {
                    if inplace {
                        scope_inner.definitions.definitions[start_index].do_expand = true;
                        xtokens.push(XToken::IndexedSubstitution(Dollar::new(), start_index));
                    }
                    start_index += 1;
                }
            }

            // catch definitions that fallen through, this happens on syntax errors
            Ok(XToken::DefinitionError(_, def)) => {
                return Error::other(
                    input,
                    format!("definition/table syntax: {}", def.tokens_to_string()),
                );
            }

            // resolve names
            Ok(XToken::NamedSubstitution(d, i)) => {
                let index = scope_inner.definitions.copy_lookup(i, input.counter())?;
                scope_inner.definitions.definitions[index].do_expand = true;

                xtokens.push(XToken::IndexedSubstitution(d, index));
            }

            Ok(XToken::IndexOfNamed(op, i)) => {
                let index = scope_inner.definitions.copy_lookup(i, input.counter())?;
                scope_inner.definitions.definitions[index].do_expand = true;
                xtokens.push(XToken::IndexOfIndexed(op, index));
            }

            Ok(t @ XToken::IndexOfIndexed(_, index)) => {
                scope_inner.definitions.definitions[index].do_expand = true;
                xtokens.push(t);
            }

            Ok(XToken::LengthOfNamed(op, i)) => {
                let index = scope_inner.definitions.copy_lookup(i, input.counter())?;
                xtokens.push(XToken::LengthOfIndexed(op, index));
            }

            Ok(XToken::Directive(_, directive)) => {
                // NOTE: for now we dont support any directives but leave this syntax reserved for future
                let id = directive.as_str();
                return Error::other(
                    input,
                    format!(
                        "unknown directive {:?}: at {:?}",
                        id,
                        directive.span().start(),
                    ),
                );
            }

            Ok(XToken::BreakSemicolon(_)) => {
                xtokens.push(XToken::TokenTree(PunctAlone::<';'>::new().into()));
                if break_at_item {
                    break;
                }
            }

            Ok(XToken::BreakPunct(p, _)) => {
                xtokens.push(XToken::TokenTree(TokenTree::Punct(Punct::new(
                    p.as_char(),
                    Spacing::Alone,
                ))));
            }

            Ok(XToken::IndexedSubstitution(op, index)) => {
                scope_inner.definitions.definitions[index].do_expand = true;
                xtokens.push(XToken::IndexedSubstitution(op, index));
            }

            Ok(XToken::ConcatIdent(op, ident, dollar, def)) => {
                let index = def.fold2(Ok, |name| {
                    let index = scope_inner.definitions.copy_lookup(name, input.counter())?;
                    scope_inner.definitions.definitions[index].do_expand = true;
                    Ok(index)
                })?;

                xtokens.push(XToken::ConcatIdent(op, ident, dollar, Either::First(index)));
            }

            // The things that are moved verbatim
            Ok(
                t @ (XToken::TokenTree(_) | XToken::LengthOfIndexed(..) | XToken::EscapedDollar(_)),
            ) => {
                xtokens.push(t);
            }

            // prevent parsing a empty TokenStream, remaining cases
            Ok(XToken::EndOfStream(_)) => break,

            // TODO: remove? Err(_) if EndOfStream::parse(input).is_ok() => break,
            Err(e) => return Err(e),

            // The cases that will never happen when parsing
            Ok(XToken::XGroup(_) | XToken::TokenStream(_)) => unreachable!(),
        }
    }
    Ok(xtokens)
}

#[test]
fn smoke_empty() {
    let mut tokens = "".into_token_iter();
    let xscope = XMacro::parse_all(&mut tokens).expect("XMacro");
    assert!(xscope.xtokens.is_empty());
}

#[test]
fn smoke_some_tokens() {
    let mut tokens = r#"this (is a) "test""#.into_token_iter();
    let xscope = XMacro::parse_all(&mut tokens).expect("XMacro");
    assert!((xscope.xtokens.len()) == (3));
}

impl ToTokens for XMacro<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        // Works only on the root scope, all other scopes are expanded in place
        for token in &self.xtokens {
            token.to_tokens(tokens);
        }
    }
}

#[derive(Debug, Clone)]
pub struct Definition<'a> {
    // The content of the definition
    pub def: Cow<'a, [TokenStream]>,
    pub do_expand: bool,
    pub group_id: u32,
}

/// Definitions
#[derive(Debug)]
#[allow(clippy::struct_field_names)]
pub struct Definitions<'a> {
    // Parent scope
    parent: Option<&'a Definitions<'a>>,
    // Definitions in the current scope
    pub definitions: Vec<Definition<'a>>,
    // Names in the current scope
    names: HashMap<CachedIdent, usize>,
}

impl<'a> Definitions<'a> {
    fn new(parent: Option<&'a Self>) -> Self {
        Self {
            parent,
            definitions: Vec::new(),
            names: HashMap::new(),
        }
    }
}

impl Definitions<'_> {
    fn extend(&mut self, definitions: DefinitionContent, pos: usize) -> Result<()> {
        let DefinitionContent { names, definitions } = definitions;
        let index_start = self.definitions.len();
        let group_id = new_group_id();
        self.definitions
            .extend(definitions.into_iter().map(|def| Definition {
                def,
                do_expand: false,
                group_id,
            }));

        for (index, name) in names.into_iter().enumerate() {
            if self
                .names
                .insert(name.name.clone(), index_start + index)
                .is_some()
            {
                return Error::other(
                    pos,
                    format!(
                        "name {:?} already defined: at {:?}",
                        &name,
                        &name.name.span().start(),
                    ),
                );
            }
        }
        Ok(())
    }

    /// Lookup a name in the current scope or in the parent scopes.
    /// When found in a parent scope it is copied up to the current scope.
    fn copy_lookup(&mut self, name: CachedIdent, pos: usize) -> Result<usize> {
        if let Some(index) = self.names.get(&name) {
            Ok(*index)
        } else {
            let mut parent = self.parent;
            while let Some(p) = parent {
                if let Some(index) = p.names.get(&name) {
                    // copy the definition up
                    self.definitions.push(Definition {
                        def: Cow::Borrowed(&p.definitions[*index].def),
                        do_expand: p.definitions[*index].do_expand,
                        group_id: p.definitions[*index].group_id,
                    });

                    let index = self.definitions.len() - 1;
                    self.names.insert(name, index);
                    return Ok(index);
                }
                parent = p.parent;
            }
            Error::other(
                pos,
                format!(
                    "'${}' not defined: at {:?}",
                    name.as_str(),
                    &name.span().start(),
                ),
            )
        }
    }
}

/// This is used to generate unique group ids
fn new_group_id() -> u32 {
    static GROUP_CNT: AtomicU32 = AtomicU32::new(0);
    GROUP_CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}