brink-ir 0.0.3

Intermediate representations for inkle's ink narrative scripting language
Documentation
//! Body dispatchers and child classifiers.
//!
//! Defines child classification enums for different body contexts and
//! the dispatcher functions that wire classifiers to the accumulator.

use brink_syntax::SyntaxKind;
use brink_syntax::ast::{self, AstNode};

use crate::Block;

use super::content::{ContentAccumulator, DirectBackend};
use super::context::{LowerScope, LowerSink};

// ─── Weave-context child classification ─────────────────────────────

/// Children in a weave body context (knot, stitch, source file root).
pub enum BodyChild {
    ContentLine(ast::ContentLine),
    LogicLine(ast::LogicLine),
    TagLine(ast::TagLine),
    DivertNode(ast::DivertNode),
    InlineLogic(ast::InlineLogic),
    MultilineBlock(ast::MultilineBlock),
    Choice(ast::Choice),
    Gather(ast::Gather),
    Structural,
    Trivia,
}

/// Classify a CST child in weave body context.
pub fn classify_body_child(node: &brink_syntax::SyntaxNode) -> BodyChild {
    match node.kind() {
        SyntaxKind::CONTENT_LINE => {
            ast::ContentLine::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::ContentLine)
        }
        SyntaxKind::LOGIC_LINE => {
            ast::LogicLine::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::LogicLine)
        }
        SyntaxKind::TAG_LINE => {
            ast::TagLine::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::TagLine)
        }
        SyntaxKind::DIVERT_NODE => {
            ast::DivertNode::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::DivertNode)
        }
        SyntaxKind::INLINE_LOGIC => {
            ast::InlineLogic::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::InlineLogic)
        }
        SyntaxKind::MULTILINE_BLOCK => ast::MultilineBlock::cast(node.clone())
            .map_or(BodyChild::Trivia, BodyChild::MultilineBlock),
        SyntaxKind::CHOICE => {
            ast::Choice::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::Choice)
        }
        SyntaxKind::GATHER => {
            ast::Gather::cast(node.clone()).map_or(BodyChild::Trivia, BodyChild::Gather)
        }
        // Structural children — handled by the parent, skipped here.
        SyntaxKind::KNOT_DEF
        | SyntaxKind::KNOT_HEADER
        | SyntaxKind::STITCH_DEF
        | SyntaxKind::STITCH_HEADER
        | SyntaxKind::VAR_DECL
        | SyntaxKind::CONST_DECL
        | SyntaxKind::LIST_DECL
        | SyntaxKind::EXTERNAL_DECL
        | SyntaxKind::INCLUDE_STMT
        | SyntaxKind::STRAY_CLOSING_BRACE
        | SyntaxKind::AUTHOR_WARNING
        // Choice/gather structural parts (when iterating a choice node's children).
        | SyntaxKind::CHOICE_BULLETS
        | SyntaxKind::LABEL
        | SyntaxKind::CHOICE_CONDITION
        | SyntaxKind::CHOICE_START_CONTENT
        | SyntaxKind::CHOICE_BRACKET_CONTENT
        | SyntaxKind::CHOICE_INNER_CONTENT
        | SyntaxKind::TAGS
        | SyntaxKind::MIXED_CONTENT
        | SyntaxKind::GATHER_DASHES => BodyChild::Structural,
        // EMPTY_LINE is trivia; ERROR nodes appear on malformed input (already
        // diagnosed by the parser) — skip them gracefully rather than panicking,
        // the diagnostic still fails the compile.
        SyntaxKind::EMPTY_LINE | SyntaxKind::ERROR => BodyChild::Trivia,
        other => {
            debug_assert!(
                other.is_trivia(),
                "unexpected SyntaxKind in classify_body_child: {other:?}"
            );
            BodyChild::Trivia
        }
    }
}

// ─── Branch-context child classification ────────────────────────────

/// Children in a branch body context (branchless conditional, multiline
/// branch body). Includes raw token-level children (text, newline, etc.).
pub enum BranchChild {
    ContentLine(ast::ContentLine),
    LogicLine(ast::LogicLine),
    TagLine(ast::TagLine),
    DivertNode(ast::DivertNode),
    InlineLogic(ast::InlineLogic),
    Choice(ast::Choice),
    Text(String),
    Glue,
    Escape(String),
    Newline,
    Whitespace(String),
    /// `ELSE_BRANCH` — signals stop for branchless bodies.
    Stop,
    Trivia,
}

/// Classify children of a branch body (both tokens and nodes).
pub fn classify_branch_child(
    child: &rowan::NodeOrToken<brink_syntax::SyntaxNode, brink_syntax::SyntaxToken>,
) -> BranchChild {
    match child {
        rowan::NodeOrToken::Token(token) => match token.kind() {
            SyntaxKind::NEWLINE => BranchChild::Newline,
            SyntaxKind::WHITESPACE => BranchChild::Whitespace(token.text().to_string()),
            _ => BranchChild::Trivia,
        },
        rowan::NodeOrToken::Node(node) => match node.kind() {
            SyntaxKind::CONTENT_LINE => ast::ContentLine::cast(node.clone())
                .map_or(BranchChild::Trivia, BranchChild::ContentLine),
            SyntaxKind::LOGIC_LINE => ast::LogicLine::cast(node.clone())
                .map_or(BranchChild::Trivia, BranchChild::LogicLine),
            SyntaxKind::TAG_LINE => {
                ast::TagLine::cast(node.clone()).map_or(BranchChild::Trivia, BranchChild::TagLine)
            }
            SyntaxKind::DIVERT_NODE => ast::DivertNode::cast(node.clone())
                .map_or(BranchChild::Trivia, BranchChild::DivertNode),
            SyntaxKind::INLINE_LOGIC => ast::InlineLogic::cast(node.clone())
                .map_or(BranchChild::Trivia, BranchChild::InlineLogic),
            SyntaxKind::CHOICE => {
                ast::Choice::cast(node.clone()).map_or(BranchChild::Trivia, BranchChild::Choice)
            }
            SyntaxKind::ELSE_BRANCH => BranchChild::Stop,
            SyntaxKind::TEXT => {
                let text = node.text().to_string();
                if text.is_empty() {
                    BranchChild::Trivia
                } else {
                    BranchChild::Text(text)
                }
            }
            SyntaxKind::GLUE_NODE => BranchChild::Glue,
            SyntaxKind::ESCAPE => {
                let text = node.text().to_string();
                if text.len() > 1 {
                    BranchChild::Escape(text)
                } else {
                    BranchChild::Trivia
                }
            }
            // Error-recovery nodes appear on malformed input (the parser has
            // already diagnosed them); skip rather than panic.
            SyntaxKind::ERROR => BranchChild::Trivia,
            other if other.is_token() => BranchChild::Trivia,
            other => {
                debug_assert!(
                    other.is_trivia(),
                    "unexpected SyntaxKind in classify_branch_child: {other:?}"
                );
                BranchChild::Trivia
            }
        },
    }
}

// ─── Simple body dispatcher (for backbone tests) ────────────────────

/// Lower a simple body (content lines + logic lines, no weave) to a [`Block`].
pub fn lower_simple_body(
    parent: &brink_syntax::SyntaxNode,
    scope: &LowerScope,
    sink: &mut impl LowerSink,
) -> Block {
    let mut acc = ContentAccumulator::new(DirectBackend::new());

    for child in parent.children() {
        match classify_body_child(&child) {
            BodyChild::ContentLine(cl) => {
                acc.handle(&cl, scope, sink);
            }
            BodyChild::LogicLine(ll) => {
                acc.handle(&ll, scope, sink);
            }
            BodyChild::TagLine(tl) => {
                acc.handle(&tl, scope, sink);
            }
            BodyChild::DivertNode(dn) => {
                acc.handle(&dn, scope, sink);
            }
            BodyChild::InlineLogic(il) => {
                acc.handle(&il, scope, sink);
            }
            BodyChild::MultilineBlock(mb) => {
                acc.handle(&mb, scope, sink);
            }
            BodyChild::Choice(_)
            | BodyChild::Gather(_)
            | BodyChild::Structural
            | BodyChild::Trivia => {}
        }
    }

    acc.finish()
}