stack-compiler 0.1.0

Reference compiler frontend for the Stack diagram language
Documentation
use stack_compiler::{
    compile, compile_bytes, compile_with_source_map, diagnostic::Severity, ir, lossless::TokenKind,
    parse_lossless, source_map::SourceOrigin,
};

#[test]
fn public_api_applies_defaults_to_a_valid_document() {
    let output = compile("stack 1.0 diagram \"API\" { node api \"API\" }");
    assert!(output.diagram.is_some(), "{:?}", output.diagnostics);
    let Some(diagram) = output.diagram else {
        return;
    };

    assert_eq!(diagram.theme_id, "default");
    assert_eq!(diagram.nodes.len(), 1);
    let Some(node) = diagram.nodes.first() else {
        return;
    };
    assert_eq!(node.kind, ir::NodeKind::Service);
    assert!(diagram.edges.is_empty());
}

#[test]
fn compiler_errors_prevent_ir() {
    let output = compile(
        r#"stack 1.0
diagram "Invalid" {
  group empty "Empty" {}
  node api "API"
  edge api -> missing
}"#,
    );

    assert!(output.diagram.is_none());
    assert!(
        output
            .diagnostics
            .iter()
            .all(|diagnostic| diagnostic.severity == Severity::Error)
    );
    assert!(
        output
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "STK3003")
    );
    assert!(
        output
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "STK3009")
    );
}

#[test]
fn byte_api_reports_encoding_and_bom_errors() {
    let invalid_utf8 = compile_bytes(b"stack 1.0\n\xff");
    assert_eq!(invalid_utf8.diagnostics[0].code, "STK1001");
    assert!(invalid_utf8.diagram.is_none());

    let bom = compile_bytes("\u{feff}stack 1.0 diagram \"x\" { node x \"X\" }".as_bytes());
    assert_eq!(bom.diagnostics[0].code, "STK1002");
    assert!(bom.diagram.is_none());
}

#[test]
fn public_lossless_api_reconstructs_authored_source() {
    let source = concat!(
        "// leading\r\n",
        "stack 1.0\r\n",
        "diagram \"\\u0041\" { node api \"API\" } // trailing\r\n",
    );
    let output = parse_lossless(source);
    assert!(output.diagnostics.is_empty());
    let Some(document) = output.document else {
        return;
    };

    assert_eq!(document.reconstruct(), source);
    assert!(
        document
            .tokens()
            .iter()
            .any(|token| token.kind == TokenKind::LineComment)
    );
    assert!(document.tokens().iter().any(
        |token| matches!(&token.kind, TokenKind::String(value) if value == "A")
            && token.text == "\"\\u0041\""
    ));
}

#[test]
fn public_source_map_api_distinguishes_authored_and_default_values() {
    let source = concat!(
        "stack 1.0\n",
        "diagram \"Mapped\" {\n",
        "  node api \"API\" { icon \"service\" }\n",
        "  node worker \"Worker\"\n",
        "  layout { order [api, worker] }\n",
        "}\n",
    );
    let output = compile_with_source_map(source);
    assert!(output.diagnostics.is_empty());
    let Some(source_map) = output.source_map else {
        return;
    };

    assert!(matches!(
        source_map.node_icon("api"),
        Some(SourceOrigin::Authored(_))
    ));
    assert_eq!(source_map.node_icon("worker"), Some(SourceOrigin::Omitted));
    assert!(matches!(
        source_map.diagram_order(),
        SourceOrigin::Authored(_)
    ));
}

#[test]
fn complexity_errors_and_degree_warnings_have_distinct_outcomes() {
    let no_nodes = compile("stack 1.0 diagram \"Empty\" {}");
    assert!(no_nodes.diagram.is_none());
    assert_eq!(no_nodes.diagnostics[0].code, "STK4003");

    let mut dense = String::from("stack 1.0\ndiagram \"Dense\" {\n  node hub \"Hub\"\n");
    for index in 0..13 {
        dense.push_str(&format!("  node n{index} \"N {index}\"\n"));
        dense.push_str(&format!("  edge hub -> n{index}\n"));
    }
    dense.push('}');
    let warning = compile(&dense);
    assert!(warning.diagram.is_some());
    assert_eq!(warning.diagnostics[0].code, "STK4002");
    assert_eq!(warning.diagnostics[0].severity, Severity::Warning);
}