aufbau 0.3.1

Generalized prefix parsing for a class of context-dependent languages
use super::*;
use crate::engine::grammar::SPG;
use crate::engine::parse::arena::{ChildRef, Lexeme};
use crate::engine::structure::ast::FusionAST;
use crate::typing::Context;
use crate::typing::TypingSynth;

fn assert_status_model(ast: &FusionAST, grammar: &SPG) {
    let arena = ast.arena();
    let input_end = ast.segs().len();

    for id in 0..arena.node_count() {
        let node = arena.node(id).expect("arena node should exist");
        let status = node.status;
        let alts = arena.alts_for(id).expect("node should have alternatives");

        assert!(!alts.is_empty(), "node {id} has no packed alternatives");

        for alt in alts.iter() {
            let rhs_len = grammar
                .prod(alt.prod)
                .map(|prod| prod.rhs.len())
                .expect("packed alternative should reference a production");
            let all_children_complete = alt.children.iter().all(|child| arena.complete(child));
            let production_fully_recognized =
                alt.children.len() == rhs_len && all_children_complete;
            let any_child_open = alt.children.iter().any(|child| arena.open(child));

            for child in &alt.children {
                if let ChildRef::Terminal(Lexeme {
                    matched,
                    open: true,
                    ..
                }) = child
                {
                    assert_eq!(
                        matched.end as usize, input_end,
                        "terminal openness must only occur at the right frontier"
                    );
                }
            }

            match status {
                NodeStatus::Exact => {
                    assert!(
                        production_fully_recognized && !any_child_open,
                        "Closed node {id} must be fully recognized with no open child"
                    );
                }
                NodeStatus::Extensible => {
                    assert!(
                        production_fully_recognized && any_child_open,
                        "Extensible node {id} must be fully recognized with an open child"
                    );
                }
                NodeStatus::Prefix => {
                    assert!(
                        !production_fully_recognized,
                        "Partial node {id} must not be fully recognized"
                    );
                }
            }
        }
    }
}

#[test]
fn interior_extensible_terminal_does_not_make_wrapper_open() {
    let grammar = SPG::load("Word ::= /[a-z]+/\nStart ::= Word ':' '!'").unwrap();
    let mut parser = stub_parser(grammar);
    let ast = parser.parse("foo : !", 0).unwrap();
    let arena = ast.arena();
    let root_statuses: Vec<_> = ast
        .root_ids()
        .iter()
        .filter_map(|&id| arena.node(id).map(|node| node.status))
        .collect();

    assert!(
        ast.is_complete(),
        "interior regex terminal should not force inexact root: {:?}",
        root_statuses
    );

    assert!(
        (0..arena.node_count()).any(|id| {
            arena.node(id).is_some_and(|node| {
                node.span.start == 0 && node.span.end == 1 && node.status == NodeStatus::Exact
            })
        }),
        "interior regex child should be closed away from EOF"
    );
    assert!(ast.root_ids().iter().any(|&id| {
        arena
            .node(id)
            .is_some_and(|node| node.status == NodeStatus::Exact)
    }));
}

#[test]
fn eof_extensible_terminal_is_accepted_but_not_exact() {
    let grammar = SPG::load("Start ::= /[a-z]+/").unwrap();
    let mut parser = stub_parser(grammar);
    let ast = parser.parse("foo", 0).unwrap();

    assert!(
        ast.is_complete(),
        "single extensible token at EOF should count as complete prefix"
    );
    assert!(
        !ast.is_exact(),
        "single extensible token at EOF is not exact"
    );

    let arena = ast.arena();
    assert!(ast.root_ids().iter().any(|&id| {
        arena
            .node(id)
            .is_some_and(|node| node.status == NodeStatus::Extensible)
    }));
}

#[test]
fn prefix_match_inside_input_is_dead_not_frontier_open() {
    let grammar = SPG::load("Start ::= 'false' '!' ").unwrap();
    let mut parser = stub_parser(grammar);
    assert!(
        parser.parse("f !", 0).is_err(),
        "mid-input prefix of a terminal should be dead, not a frontier candidate"
    );
}

#[test]
fn exact_parse_requires_closed_root() {
    let grammar = SPG::load("Start ::= 'x'").unwrap();
    let mut synth = TypingSynth::new(grammar, "x");
    let ast = synth.parse_with(&Context::new()).unwrap();
    let arena = ast.arena();
    let root_statuses: Vec<_> = ast
        .root_ids()
        .iter()
        .filter_map(|&id| arena.node(id).map(|node| node.status))
        .collect();
    assert!(
        ast.is_complete(),
        "expected closed literal root, got {:?}",
        root_statuses
    );
    assert!(ast.is_exact(), "closed literal root should be exact");
}

#[test]
fn parser_nodes_satisfy_documented_status_model() {
    let cases = [
        ("Start ::= 'x'", "x"),
        ("Start ::= /[a-z]+/", "foo"),
        ("Start ::= 'x' 'y'", "x"),
        ("Word ::= /[a-z]+/\nStart ::= Word ':' Word", "foo : bar"),
    ];

    for (spec, input) in cases {
        let grammar = SPG::load(spec).unwrap();
        let mut parser = stub_parser(grammar.clone());
        let ast = parser.parse(input, 0).unwrap();

        assert_status_model(&ast, &grammar);
    }
}