plotnik-compiler 0.3.2

Compiler for Plotnik query language (parser, analyzer, bytecode emitter)
Documentation
use plotnik_bytecode::{Module, dump};
use plotnik_core::Colors;
use plotnik_langs::{Lang, from_name};

use crate::SourceMap;

use super::{LinkedQuery, QueryAnalyzed, QueryBuilder};

fn javascript() -> Lang {
    from_name("javascript").expect("javascript lang")
}

macro_rules! expect_invalid {
    ($($name:literal: $content:literal),+ $(,)?) => {{
        let mut source_map = SourceMap::new();
        $(source_map.add_file($name, $content);)+
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();
        if query.is_valid() {
            panic!("Expected invalid query, got valid");
        }
        query.dump_diagnostics()
    }};
}

impl QueryAnalyzed {
    #[track_caller]
    fn parse_and_validate(src: &str) -> Self {
        let source_map = SourceMap::one_liner(src);
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();
        if !query.is_valid() {
            panic!(
                "Expected valid query, got error:\n{}",
                query.dump_diagnostics()
            );
        }
        query
    }

    /// Parse and validate syntax only (no semantic analysis).
    /// Use this for pure parser/grammar tests.
    #[track_caller]
    fn parse_syntax_only(src: &str) -> Self {
        use crate::diagnostics::DiagnosticKind::*;
        let source_map = SourceMap::one_liner(src);
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();
        // Only check for parse errors (not semantic errors)
        let diag = query.diagnostics();
        let has_parse_error = diag.raw().iter().any(|d| {
            matches!(
                d.kind,
                UnclosedTree
                    | UnclosedSequence
                    | UnclosedAlternation
                    | UnclosedRegex
                    | ExpectedExpression
                    | ExpectedTypeName
                    | ExpectedCaptureName
                    | ExpectedFieldName
                    | ExpectedSubtype
                    | ExpectedPredicateValue
                    | EmptyTree
                    | EmptyAnonymousNode
                    | EmptySequence
                    | EmptyAlternation
                    | BareIdentifier
                    | InvalidSeparator
                    | UnexpectedToken
            )
        });

        if has_parse_error {
            panic!(
                "Expected valid syntax, got parse error:\n{}",
                query.dump_diagnostics()
            );
        }
        query
    }

    #[track_caller]
    pub fn expect(src: &str) -> Self {
        let source_map = SourceMap::one_liner(src);
        QueryBuilder::new(source_map).parse().unwrap().analyze()
    }

    #[track_caller]
    pub fn expect_valid(src: &str) -> Self {
        Self::parse_and_validate(src)
    }

    #[track_caller]
    pub fn expect_valid_cst(src: &str) -> String {
        Self::parse_and_validate(src).dump_cst()
    }

    /// Parse-only CST dump (for pure parser tests, no semantic validation).
    #[track_caller]
    pub fn parse_cst(src: &str) -> String {
        Self::parse_syntax_only(src).dump_cst()
    }

    #[track_caller]
    pub fn expect_valid_cst_full(src: &str) -> String {
        Self::parse_and_validate(src).dump_cst_full()
    }

    #[track_caller]
    pub fn expect_valid_ast(src: &str) -> String {
        Self::parse_and_validate(src).dump_ast()
    }

    #[track_caller]
    pub fn expect_valid_arities(src: &str) -> String {
        Self::parse_and_validate(src).dump_with_arities()
    }

    #[track_caller]
    pub fn expect_valid_symbols(src: &str) -> String {
        Self::parse_and_validate(src).dump_symbols()
    }

    #[track_caller]
    pub fn expect_valid_linking(src: &str) -> LinkedQuery {
        let query = Self::parse_and_validate(src).link(&javascript());
        if !query.is_valid() {
            panic!(
                "Expected valid linking, got error:\n{}",
                query.dump_diagnostics()
            );
        }
        query
    }

    #[track_caller]
    pub fn expect_invalid_linking(src: &str) -> String {
        let query = Self::parse_and_validate(src).link(&javascript());
        if query.is_valid() {
            panic!("Expected failed linking, got valid");
        }
        query.dump_diagnostics()
    }

    #[track_caller]
    pub fn expect_valid_types(src: &str) -> String {
        let query = Self::parse_and_validate(src).link(&javascript());
        if !query.is_valid() {
            panic!(
                "Expected valid types, got error:\n{}",
                query.dump_diagnostics()
            );
        }

        // Emit to bytecode and then emit TypeScript from the bytecode module
        let bytecode = query.emit().expect("bytecode emission should succeed");
        let module = Module::load(&bytecode).expect("module loading should succeed");
        crate::typegen::typescript::emit(&module)
    }

    #[track_caller]
    pub fn expect_valid_bytecode(src: &str) -> String {
        let query = Self::parse_and_validate(src).link(&javascript());
        if !query.is_valid() {
            panic!(
                "Expected valid linking, got error:\n{}",
                query.dump_diagnostics()
            );
        }
        let bytecode = query.emit().expect("bytecode emission should succeed");
        let module = Module::load(&bytecode).expect("module loading should succeed");
        dump(&module, Colors::OFF)
    }

    #[track_caller]
    pub fn expect_valid_bytes(src: &str) -> Vec<u8> {
        let query = Self::parse_and_validate(src).link(&javascript());
        if !query.is_valid() {
            panic!(
                "Expected valid linking, got error:\n{}",
                query.dump_diagnostics()
            );
        }
        query.emit().expect("bytecode emission should succeed")
    }

    #[track_caller]
    pub fn expect_invalid(src: &str) -> String {
        let source_map = SourceMap::one_liner(src);
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();
        if query.is_valid() {
            panic!("Expected invalid query, got valid:\n{}", query.dump_cst());
        }
        query.dump_diagnostics()
    }

    #[track_caller]
    pub fn expect_warning(src: &str) -> String {
        let source_map = SourceMap::one_liner(src);
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();

        if !query.is_valid() {
            panic!(
                "Expected valid query with warning, got error:\n{}",
                query.dump_diagnostics()
            );
        }

        if !query.diagnostics().has_warnings() {
            panic!("Expected warning, got none:\n{}", query.dump_cst());
        }

        query.dump_diagnostics()
    }

    #[track_caller]
    pub fn expect_cst_with_warnings(src: &str) -> String {
        let source_map = SourceMap::one_liner(src);
        let query = QueryBuilder::new(source_map).parse().unwrap().analyze();

        if !query.is_valid() {
            panic!(
                "Expected valid query (warnings ok), got error:\n{}",
                query.dump_diagnostics()
            );
        }

        query.dump_cst()
    }
}

#[test]
fn invalid_three_way_mutual_recursion_across_files() {
    let res = expect_invalid! {
        "a.ptk": "A = (a (B))",
        "b.ptk": "B = (b (C))",
        "c.ptk": "C = (c (A))",
    };

    insta::assert_snapshot!(res, @r"
    error: infinite recursion: no escape path
     --> c.ptk:1:9
      |
    1 | C = (c (A))
      | -       ^
      | |       |
      | |       references A
      | C is defined here
      |
     ::: a.ptk:1:9
      |
    1 | A = (a (B))
      |         - references B
      |
     ::: b.ptk:1:9
      |
    1 | B = (b (C))
      |         - references C (completing cycle)
      |
    help: add a non-recursive branch to terminate: `[Base: ... Rec: (Self)]`
    ");
}

#[test]
fn multifile_field_with_ref_to_seq_error() {
    let res = expect_invalid! {
        "defs.ptk": "X = {(a) (b)}",
        "main.ptk": "Q = (call name: (X))",
    };

    insta::assert_snapshot!(res, @r"
    error: field `name` cannot match a sequence
     --> main.ptk:1:17
      |
    1 | Q = (call name: (X))
      |                 ^^^
      |
     ::: defs.ptk:1:5
      |
    1 | X = {(a) (b)}
      |     --------- defined here
    ");
}