zirium 0.0.13

Lossless textual MLIR syntax parsing and semantic processing
Documentation
use zirium::query::Query;
use zirium::query::{
    lexer::{
        DiagnosticKind as LexDiagnosticKind, MAX_QUERY_BYTES, TokenKind, lex, query_size_supported,
    },
    parser::{Predicate, Program, Stage, parse, parse_with_nesting_limit},
};

fn initial_predicate(program: &Program) -> &Predicate {
    let Stage::Filter { predicate, .. } = &program.expression().first[0] else {
        panic!("expected filter")
    };
    predicate
}

#[test]
fn query_boundaries_and_edit_validation() {
    for source in [
        "",
        "# just a comment",
        "input",
        "emit",
        "emit | input",
        "count",
        "filter(true)",
        "input | filter(false) | subtree | emit",
        "filter(op(\"x\")) | root(op(\"func.func\")) | unique | attr(\"sym_name\") | json",
        "fixpoint(closure)",
        "fixpoint(closure | emit)",
        "(defs | emit) union users",
        "(defs union users) | filter(true)",
        "fixpoint(filter(true) union users)",
    ] {
        Query::parse(source).unwrap_or_else(|error| panic!("{source}: {error}"));
    }
    for source in [
        "count | root",
        "(count) | defs",
        "fixpoint(count)",
        "fixpoint(set_attr(\"tag\", \"x\"))",
        "input union (filter(true) | count)",
        "input union (set_attr(\"tag\", \"x\"))",
        "select(op(\"x\"))",
        "filter(true) | union(op(\"x\"))",
        "filter(true) |",
        "()",
        "fixpoint()",
    ] {
        assert!(Query::parse(source).is_err(), "{source}");
    }
    for source in [
        r#"set_attr("bad name", "hot")"#,
        r#"remove_attr("bad name")"#,
    ] {
        assert!(
            Query::parse(source)
                .unwrap_err()
                .to_string()
                .contains("dotted ASCII identifier")
        );
    }
    assert!(
        Query::parse("set_attr(\"tag\", \"hot\nvalue\")")
            .unwrap_err()
            .to_string()
            .contains("control characters")
    );
    Query::parse(r#"set_attr("tag", "quoted \"value\" \\ path")"#).unwrap();
}

#[test]
fn lexer_records_token_kinds_ranges_and_string_failures() {
    let source = "op (\"a\\\"b\") |\nroot";
    let lexed = lex(source);
    let kinds = lexed
        .tokens()
        .iter()
        .map(|token| token.kind())
        .collect::<Vec<_>>();
    assert_eq!(
        kinds,
        [
            TokenKind::Identifier,
            TokenKind::Trivia,
            TokenKind::LParen,
            TokenKind::String,
            TokenKind::RParen,
            TokenKind::Trivia,
            TokenKind::Pipe,
            TokenKind::Trivia,
            TokenKind::Identifier,
            TokenKind::Eof,
        ]
    );
    assert_eq!(lexed.tokens()[3].range().as_range(), 4..10);
    assert!(lexed.diagnostics().is_empty());

    let invalid = lex("\"bad\\q\" @");
    assert_eq!(
        invalid.diagnostics()[0].kind(),
        LexDiagnosticKind::InvalidEscape
    );
    assert_eq!(invalid.diagnostics()[0].range().as_range(), 4..6);
    assert_eq!(
        invalid.diagnostics()[1].kind(),
        LexDiagnosticKind::InvalidToken
    );
    let trailing_escape = lex("\"bad\\");
    assert!(
        trailing_escape
            .diagnostics()
            .iter()
            .any(|diagnostic| diagnostic.kind() == LexDiagnosticKind::InvalidEscape)
    );
}

#[test]
fn parser_builds_ranged_syntax_with_insignificant_whitespace() {
    let source = " filter ( op ( \"arith.addi\" ) )\n| set_attr ( \"tag\" , \"hot\" ) | count ";
    let lexed = lex(source);
    let parsed = parse(&lexed);
    assert!(parsed.diagnostics().is_empty());
    let program = parsed.program().unwrap();
    assert_eq!(program.range().as_range(), 1..source.len());
    assert!(
        matches!(initial_predicate(program), Predicate::Op { name, .. } if name == "arith.addi")
    );
    assert!(
        matches!(&program.expression().first[1..], [Stage::SetAttr { name, value, .. }, Stage::Count { .. }] if name == "tag" && value == "hot")
    );
}

#[test]
fn parser_builds_ranged_remove_attr_stage() {
    let source = r#"filter(op("x")) | remove_attr("analysis.tag")"#;
    let parsed = parse(&lex(source));
    assert!(parsed.diagnostics().is_empty());
    assert!(matches!(
        &parsed.program().unwrap().expression().first[1..],
        [Stage::RemoveAttr { name, range }] if name == "analysis.tag" && range.as_range() == (18..source.len())
    ));
}

#[test]
fn parser_builds_ranged_relationship_stages() {
    let source = r#"filter(op("x")) | defs | users | parent | children | subtree | unique"#;
    let parsed = parse(&lex(source));
    assert!(parsed.diagnostics().is_empty());
    assert!(matches!(
        &parsed.program().unwrap().expression().first[1..],
        [
            Stage::Defs { .. },
            Stage::Users { .. },
            Stage::Parent { .. },
            Stage::Children { .. },
            Stage::Subtree { .. },
            Stage::Unique { .. }
        ]
    ));
    assert_eq!(
        parsed.program().unwrap().expression().first[1]
            .range()
            .as_range(),
        18..22
    );
}

#[test]
fn parser_builds_root_projection_and_json_stages() {
    let source = r#"root(op("func.func")) | attr("sym_name") | json"#;
    let parsed = parse(&lex(source));
    assert!(parsed.diagnostics().is_empty());
    assert!(matches!(
        parsed.program().unwrap().expression().first.as_slice(),
        [
            Stage::Root {
                predicate: Predicate::Op { name: operation, .. },
                ..
            },
            Stage::Attr { name, .. },
            Stage::Json { .. }
        ] if operation == "func.func" && name == "sym_name"
    ));
    assert!(Query::parse("root").is_err());
}

#[test]
fn pipes_bind_more_tightly_than_set_operators() {
    use zirium::query::parser::SetOperator;
    let parsed = parse(&lex("input | defs union users | parent except children"));
    let expression = parsed.program().unwrap().expression();
    assert_eq!(expression.first.len(), 2);
    assert!(matches!(expression.rest.as_slice(), [
        (SetOperator::Union, right), (SetOperator::Except, last)
    ] if matches!(right.as_slice(), [Stage::Users { .. }, Stage::Parent { .. }])
        && matches!(last.as_slice(), [Stage::Children { .. }])));
    let grouped = parse(&lex("(defs union users) | parent"));
    assert!(matches!(
        grouped.program().unwrap().expression().first.as_slice(),
        [Stage::Group { .. }, Stage::Parent { .. }]
    ));
}

#[test]
fn parser_builds_boolean_predicates_with_precedence_and_parentheses() {
    let parsed = parse(&lex(
        r#"filter(op("a") or has_attr("tag") and not string_attr_eq("state", "skip"))"#,
    ));
    let predicate = initial_predicate(parsed.program().unwrap());
    assert!(matches!(
        predicate,
        Predicate::Or { predicates, .. }
            if matches!(predicates.as_slice(), [Predicate::Op { name, .. }, Predicate::And { predicates, .. }]
                if name == "a" && matches!(predicates.as_slice(), [Predicate::HasAttr { .. }, Predicate::Not { predicate, .. }]
                    if matches!(predicate.as_ref(), Predicate::Attr { name, value, .. } if name == "state" && value == "skip")))
    ));

    let grouped = parse(&lex(r#"filter((op("a") or op("b")) and has_attr("tag"))"#));
    assert!(matches!(
        initial_predicate(grouped.program().unwrap()),
        Predicate::And { predicates, .. }
            if matches!(predicates.first(), Some(Predicate::Group { predicate, range })
                if matches!(predicate.as_ref(), Predicate::Or { .. }) && range.as_range() == (7..27))
    ));
}

#[test]
fn malformed_and_over_nested_predicates_are_diagnosed() {
    let missing = parse(&lex(r#"filter(string_attr_eq("tag" "value"))"#));
    assert_eq!(
        missing.diagnostics()[0].message(),
        "expected `,` in string_attr_eq"
    );

    let at_limit = parse_with_nesting_limit(&lex(r#"filter((op("x")))"#), 3);
    assert!(at_limit.diagnostics().is_empty());
    let beyond = parse_with_nesting_limit(&lex(r#"filter(((op("x"))))"#), 3);
    assert_eq!(
        beyond.diagnostics()[0].message(),
        "query nesting limit exceeded"
    );
}

#[test]
fn unary_and_boolean_chain_complexity_is_stack_safe() {
    let at_limit = format!("filter({}op(\"x\"))", "not ".repeat(62));
    assert!(
        parse_with_nesting_limit(&lex(&at_limit), 64)
            .diagnostics()
            .is_empty()
    );
    let beyond = format!("filter({}op(\"x\"))", "not ".repeat(63));
    assert_eq!(
        parse_with_nesting_limit(&lex(&beyond), 64).diagnostics()[0].message(),
        "query nesting limit exceeded"
    );
    let pathological = format!("filter({}op(\"x\"))", "not ".repeat(50_000));
    assert_eq!(
        parse(&lex(&pathological)).diagnostics()[0].message(),
        "query nesting limit exceeded"
    );

    for operator in [" or ", " and "] {
        let chain = format!(
            "filter({})",
            std::iter::repeat_n("op(\"x\")", 10_000)
                .collect::<Vec<_>>()
                .join(operator)
        );
        let parsed = parse(&lex(&chain));
        match (operator, initial_predicate(parsed.program().unwrap())) {
            (" or ", Predicate::Or { predicates, .. })
            | (" and ", Predicate::And { predicates, .. }) => {
                assert_eq!(predicates.len(), 10_000)
            }
            _ => panic!("expected a flat boolean chain"),
        }
    }
}

#[test]
fn query_nesting_is_bounded_including_modifiers_and_groups() {
    for wrapper in ["fixpoint(", "("] {
        let valid = format!("{}input{}", wrapper.repeat(63), ")".repeat(63));
        assert!(Query::parse(&valid).is_ok());
        let invalid = format!("{}input{}", wrapper.repeat(64), ")".repeat(64));
        assert!(
            Query::parse(&invalid)
                .unwrap_err()
                .message
                .contains("nesting limit")
        );
    }
    let lexed = lex("filter(op(\"x\"))");
    let parsed = parse_with_nesting_limit(&lexed, 1);
    assert_eq!(
        parsed.diagnostics()[0].message(),
        "query nesting limit exceeded"
    );
    assert_eq!(parsed.diagnostics()[0].range().as_range(), 9..10);
}

#[test]
fn lexed_input_binds_the_parser_source_and_size_checks_do_not_wrap() {
    let source = String::from("filter(op(\"é\"))");
    let lexed = lex(&source);
    let parsed = parse(&lexed);
    assert!(parsed.diagnostics().is_empty());
    assert!(
        matches!(initial_predicate(parsed.program().unwrap()), Predicate::Op { name, .. } if name == "é")
    );

    assert!(query_size_supported(MAX_QUERY_BYTES));
    assert!(!query_size_supported(MAX_QUERY_BYTES.saturating_add(1)));
}