m2-syn 0.7.1

Parser, typed CST, traversal and procedural macros for Macaulay2
Documentation
//! Integration tests for syntax wrappers.

use m2_syn::{
    Body, Delimited, Delimiter, FragmentParseError, Lex, LiteralKind, LiteralToken, Parse,
    ParseStream, Punctuated, Source, Span, Spanned, Terminated, TokenParseError, TokenStream,
    TokenTree, lex_str, parse_fragment_str, parse_native, parse_quote_m2, parse2,
};

fn assert_syntax<T: Parse + Lex>() {}

fn synthetic_span() -> Span {
    Span::unlocated(Source::synthetic(""))
}

fn integer(text: &str) -> Value {
    Value::Integer(LiteralToken::new(
        LiteralKind::Integer,
        text.to_owned(),
        synthetic_span(),
    ))
}

fn round_trip<T>(value: T, expected: &str)
where
    T: std::fmt::Debug + Eq + Parse + Lex,
{
    let tokens = value.to_token_stream();
    assert_eq!(tokens.to_string(), expected);
    assert_eq!(parse2::<T>(tokens).unwrap(), value);
}

fn parse_valid_fragment<T: Parse>(source: &str) -> T {
    parse_native(source)
        .unwrap_or_else(|error| panic!("`{source}` is not valid M2 syntax: {error}"));
    parse_fragment_str(source)
        .unwrap_or_else(|error| panic!("`{source}` does not match the test fragment: {error}"))
}

#[test]
fn fragment_string_parser_preserves_top_level_cell_boundaries() {
    assert!(matches!(
        parse_fragment_str::<Body<Punctuated<Value>>>("1;2"),
        Err(FragmentParseError::MultipleCells { count: 2 })
    ));
}

#[derive(Debug, Clone, PartialEq, Eq, m2_syn::Spanned)]
enum Value {
    Integer(LiteralToken),
}

#[test]
fn all_delimiter_macros_name_construct_parse_and_emit_their_syntax() {
    type Parenthesized = m2_syn::paren!(Value);
    type Bracketed = m2_syn::brackets!(Value);
    type Braced = m2_syn::braces!(Value);
    type AngleBars = m2_syn::angle_bars!(Value);

    assert_syntax::<Parenthesized>();
    assert_syntax::<Bracketed>();
    assert_syntax::<Braced>();
    assert_syntax::<AngleBars>();

    let span = synthetic_span();
    round_trip::<Parenthesized>(m2_syn::paren!(integer("1"), &span), "(1)");
    round_trip::<Bracketed>(m2_syn::brackets!(integer("1"), &span), "[1]");
    round_trip::<Braced>(m2_syn::braces!(integer("1"), &span), "{1}");
    round_trip::<AngleBars>(m2_syn::angle_bars!(integer("1"), &span), "<|1|>");
}

#[test]
fn delimiters_lower_cells_to_local_groups_and_unpack_groups_to_global_cells() {
    let cells = lex_str("left;right").unwrap();
    let local = m2_syn::paren!(cells, synthetic_span()).to_token_stream();
    assert_eq!(local.to_string(), "(left;right)");

    let group = match local.into_iter().next().unwrap() {
        TokenTree::Group(group) => group,
        token => panic!("expected a group, found {token:?}"),
    };
    // The group keeps its `;` as an ordinary token: nothing is lifted out of
    // the contents, so embedding the group changed none of them.
    assert_eq!(group.stream().to_string(), "left;right");
    assert!(
        group
            .stream()
            .iter()
            .any(|token| token.spelling() == Some(";"))
    );
}

#[test]
fn independently_emitted_expressions_compose_as_distinct_global_cells() {
    let expressions: [m2_syn::nodes::Expr; 2] = [parse_quote_m2!(left), parse_quote_m2!(right)];
    let file = m2_syn::parse_native("left\nright").unwrap();

    assert_eq!(expressions.len(), 2);
    assert_eq!(file.cells.len(), 2);
    assert_eq!(file.to_m2(), "left\nright");
}

#[test]
fn delimiter_atoms_themselves_parse_and_emit() {
    assert_syntax::<m2_syn::Delimiter![()]>();
    assert_syntax::<m2_syn::Delimiter![[]]>();
    assert_syntax::<m2_syn::Delimiter![{}]>();
    assert_syntax::<m2_syn::Delimiter![<| |>]>();

    let span = synthetic_span();
    round_trip(m2_syn::Delimiter![()](&span), "()");
    round_trip(m2_syn::Delimiter![[]](&span), "[]");
    round_trip(m2_syn::Delimiter![{}](&span), "{}");
    round_trip(m2_syn::Delimiter![<| |>](&span), "<||>");
}

#[test]
fn punct_macro_names_and_constructs_genuinely_punctuated_sequences() {
    type Values = m2_syn::punc!(Value);
    type ValueComma = m2_syn::SeparatedPair<Value, m2_syn::Token![,]>;

    assert_syntax::<Values>();
    assert_syntax::<ValueComma>();
    assert_syntax::<Terminated<Values>>();
    assert_syntax::<Body<Values>>();

    let comma = m2_syn::Token![,](synthetic_span());
    let two: Values = m2_syn::punc!(pairs integer("1"); comma => integer("2"));
    round_trip(two, "1,2");

    let pair = ValueComma::new(
        integer("1"),
        m2_syn::Token![,](synthetic_span()),
        integer("2"),
    );
    round_trip(pair, "1,2");

    assert!(parse_fragment_str::<Values>("1").is_err());
}

impl Parse for Value {
    fn parse(input: &mut ParseStream) -> Result<Self, TokenParseError> {
        let Some(token) = input.next_token() else {
            return Err(TokenParseError::UnexpectedEnd {
                expected: "an integer",
                span: synthetic_span(),
            });
        };
        let span = token.span();
        match token {
            TokenTree::Literal(literal) if literal.kind == LiteralKind::Integer => {
                Ok(Self::Integer(literal))
            }
            token => Err(TokenParseError::UnexpectedToken {
                expected: "an integer",
                found: token.spelling().unwrap_or("a group").to_owned(),
                span,
            }),
        }
    }
}

impl Lex for Value {
    fn to_tokens(&self, output: &mut TokenStream) {
        match self {
            Self::Integer(value) => value.to_tokens(output),
        }
    }
}

#[test]
fn generic_comma_sequences_do_not_invent_empty_elements() {
    type Parenthesized = Delimited<Punctuated<Value>, Delimiter![()]>;

    assert!(parse_fragment_str::<Parenthesized>("(,1)").is_err());
    assert!(parse_fragment_str::<Parenthesized>("(1,)").is_err());
}

#[test]
fn consuming_value_iteration_discards_commas() {
    type Parenthesized = Delimited<Punctuated<Value>, Delimiter![()]>;

    let values = parse_valid_fragment::<Parenthesized>("(1,2)")
        .contents
        .into_iter()
        .collect::<Vec<_>>();

    assert_eq!(values.len(), 2);
    assert!(matches!(values[0], Value::Integer(_)));
    assert!(matches!(values[1], Value::Integer(_)));
}

#[test]
fn semicolons_require_a_nonempty_statement_on_their_left() {
    type Parenthesized = Delimited<Body<Punctuated<Value>>, Delimiter![()]>;

    for source in ["(;)", "(1;;2)"] {
        assert!(
            parse_native(source).is_err(),
            "native parser accepted `{source}`"
        );
        assert!(
            parse_fragment_str::<Parenthesized>(source).is_err(),
            "fragment parser accepted `{source}`"
        );
    }

    let body = parse_valid_fragment::<Parenthesized>("(1,2;3,4)").contents;
    assert_eq!(body.statements.len(), 1);
    assert_eq!(body.statements[0].statement.len(), 2);
    assert_eq!(body.tail.as_ref().map(Punctuated::len), Some(2));
    assert_eq!(body.to_m2(), "1,2;3,4");
}

#[test]
fn delimited_bodies_parse_emit_and_reparse() {
    type Parenthesized = Delimited<Body<Punctuated<Value>>, Delimiter![()]>;

    let parsed: Parenthesized = parse_valid_fragment("(1,2;3,4)");
    let emitted = parsed.to_token_stream();
    let reparsed: Parenthesized = parse2(emitted.clone()).unwrap();

    assert_eq!(emitted.to_string(), "(1,2;3,4)");
    assert_eq!(reparsed.to_m2(), parsed.to_m2());
}