brink-ir 0.0.17

Intermediate representations for inkle's ink narrative scripting language
Documentation
//! `LowerBlock` impl for `ast::BranchlessCondBody`.

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

use crate::{Block, ChoiceSet, ChoiceSetContext, Stmt};

use super::super::backbone::BranchChild;
use super::super::backbone::classify_branch_child;
use super::super::choice::LowerChoice;
use super::super::content::{ContentAccumulator, DirectBackend, HandleResult};
use super::super::context::{LowerScope, LowerSink, Lowered};
use super::LowerBlock;

// ─── BranchlessCondBody ─────────────────────────────────────────────

impl LowerBlock for ast::BranchlessCondBody {
    fn lower_block(&self, scope: &LowerScope, sink: &mut impl LowerSink) -> Lowered<Block> {
        let mut acc = ContentAccumulator::new(DirectBackend::new(), scope.file_id);
        let mut is_multiline = false;
        // Issue #3507: whitespace trivia between an inline construct and
        // `<>` — see `content::helpers::push_glue`. (The else-bearing
        // sibling, `branch.rs`, already carries such whitespace as deferred
        // text, which codegen turns into the same `Spring`.)
        let mut ws_before = false;

        for child in self.syntax().children_with_tokens() {
            let classified = classify_branch_child(&child);
            let ws_before_this = std::mem::take(&mut ws_before);
            match classified {
                BranchChild::ContentLine(cl) => {
                    acc.handle(&cl, scope, sink);
                }
                BranchChild::LogicLine(ll) => {
                    acc.handle(&ll, scope, sink);
                }
                BranchChild::TagLine(tl) => {
                    acc.handle(&tl, scope, sink);
                }
                BranchChild::AnnotationLine(al) => {
                    // NS-A2: never a recognized placement in branch context.
                    super::super::directive::handle_annotation_line(&al, sink);
                }
                BranchChild::DivertNode(dn) => {
                    acc.handle(&dn, scope, sink);
                }
                BranchChild::InlineLogic(il) => {
                    let range = il.syntax().text_range();
                    if acc.handle(&il, scope, sink) == HandleResult::Inline {
                        acc.note_range(range);
                    }
                }
                BranchChild::Text(t) => {
                    let range = child.text_range();
                    acc.push_text(t, range);
                }
                BranchChild::Glue => acc.push_glue_after(child.text_range(), ws_before_this),
                BranchChild::Escape(t) => {
                    let range = child.text_range();
                    acc.push_escape(&t, range);
                }
                BranchChild::Choice(c) => {
                    acc.flush();
                    if let Ok(choice) = c.lower_choice(scope, sink) {
                        acc.push_stmt(Stmt::ChoiceSet(Box::new(ChoiceSet {
                            choices: vec![choice],
                            continuation: Block::default(),
                            context: ChoiceSetContext::Inline,
                            depth: 0,
                            gather_id: None,
                        })));
                    }
                }
                BranchChild::Whitespace(_) => ws_before = true,
                BranchChild::Trivia => {}
                BranchChild::Stop => break,

                BranchChild::Newline => {
                    let was_multiline = is_multiline;
                    is_multiline = true;
                    if acc.has_buffered_parts() {
                        let ends_glue = acc.ends_with_glue();
                        acc.flush();
                        if !ends_glue {
                            acc.push_eol();
                        }
                    } else if acc.last_was_content() || !was_multiline {
                        acc.push_eol();
                    }
                }
            }
        }

        acc.flush();
        if is_multiline && acc.last_was_content() {
            acc.push_eol();
        }
        let mut block = acc.finish();

        // In a branchless body like `{true: + A choice \n body \n -> END}`,
        // "body" and "-> END" are siblings of CHOICE in the CST, not children.
        // They end up as trailing stmts after the ChoiceSet — unreachable past
        // `done`. Move them into the last choice's body so they execute.
        move_trailing_into_choice_body(&mut block.stmts);

        Ok(block)
    }
}

/// Move any stmts after the last `ChoiceSet` into that choice's body.
fn move_trailing_into_choice_body(stmts: &mut Vec<Stmt>) {
    if let Some(pos) = stmts.iter().rposition(|s| matches!(s, Stmt::ChoiceSet(_)))
        && pos < stmts.len() - 1
    {
        let trailing: Vec<Stmt> = stmts.drain(pos + 1..).collect();
        if let Stmt::ChoiceSet(cs) = &mut stmts[pos]
            && let Some(choice) = cs.choices.last_mut()
        {
            choice.body.stmts.extend(trailing);
            // The extend may have changed the choice body's final statement
            // (docs/block-effect-model.md §10 row j) — re-derive `tail`.
            choice.body.recompute_tail();
        }
    }
}