relux 0.3.0

Expect-style integration test framework for interactive shell programs
Documentation
use crate::core::table::FileId;
use crate::diagnostics::IrSpan;
use crate::diagnostics::LoweringBail;
use crate::dsl::parser::ast::AstCleanupBlock;
use crate::dsl::parser::ast::AstShellBlock;

use super::IrNode;
use super::IrNodeLowering;
use super::LoweringContext;
use super::ident::IrIdent;
use super::stmt::IrShellStmt;

#[derive(Debug, Clone)]
pub struct IrShellBlock {
    qualifier: Option<IrIdent>,
    name: IrIdent,
    body: Vec<IrShellStmt>,
    span: IrSpan,
}

impl IrShellBlock {
    pub fn new(
        qualifier: Option<IrIdent>,
        name: IrIdent,
        body: Vec<IrShellStmt>,
        span: IrSpan,
    ) -> Self {
        Self {
            qualifier,
            name,
            body,
            span,
        }
    }

    pub fn qualifier(&self) -> Option<&IrIdent> {
        self.qualifier.as_ref()
    }

    pub fn name(&self) -> &IrIdent {
        &self.name
    }

    pub fn body(&self) -> &[IrShellStmt] {
        &self.body
    }
}

impl_ir_node_struct!(IrShellBlock);

#[derive(Debug, Clone)]
pub struct IrCleanupBlock {
    body: Vec<IrShellStmt>,
    span: IrSpan,
}

impl IrCleanupBlock {
    pub fn new(body: Vec<IrShellStmt>, span: IrSpan) -> Self {
        Self { body, span }
    }

    pub fn body(&self) -> &[IrShellStmt] {
        &self.body
    }
}

impl_ir_node_struct!(IrCleanupBlock);

impl IrNodeLowering for IrShellBlock {
    type Ast = AstShellBlock;
    fn lower(
        ast: &AstShellBlock,
        file: &FileId,
        ctx: &mut LoweringContext,
    ) -> Result<Self, LoweringBail> {
        let qualifier = ast
            .qualifier
            .as_ref()
            .map(|q| IrIdent::lower(&q.node, file, ctx))
            .transpose()?;
        let name = IrIdent::lower(&ast.name.node, file, ctx)?;
        let body = ast
            .stmts
            .iter()
            .map(|s| IrShellStmt::lower(&s.node, file, ctx))
            .collect::<Result<Vec<_>, _>>()?;
        Ok(IrShellBlock::new(
            qualifier,
            name,
            body,
            IrSpan::new(file.clone(), ast.span),
        ))
    }
}

impl IrNodeLowering for IrCleanupBlock {
    type Ast = AstCleanupBlock;
    fn lower(
        ast: &AstCleanupBlock,
        file: &FileId,
        ctx: &mut LoweringContext,
    ) -> Result<Self, LoweringBail> {
        let body = ast
            .stmts
            .iter()
            .map(|s| IrShellStmt::lower(&s.node, file, ctx))
            .collect::<Result<Vec<_>, _>>()?;
        Ok(IrCleanupBlock::new(
            body,
            IrSpan::new(file.clone(), ast.span),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::table::FileId;
    use std::path::PathBuf;

    fn test_file_id() -> FileId {
        FileId::new(PathBuf::from("test.relux"))
    }

    fn test_span() -> IrSpan {
        IrSpan::new(test_file_id(), crate::Span::new(0, 10))
    }

    fn test_ident(name: &str) -> IrIdent {
        IrIdent::new(name, test_span())
    }

    #[test]
    fn ir_shell_block() {
        let s = test_span();
        let block = IrShellBlock::new(
            None,
            test_ident("sh"),
            vec![IrShellStmt::BufferReset { span: s.clone() }],
            s,
        );
        assert_eq!(block.name().name(), "sh");
        assert_eq!(block.body().len(), 1);
    }

    #[test]
    fn ir_shell_block_empty_body() {
        let block = IrShellBlock::new(None, test_ident("sh"), vec![], test_span());
        assert!(block.body().is_empty());
    }

    #[test]
    fn ir_cleanup_block() {
        let s = test_span();
        let block = IrCleanupBlock::new(vec![IrShellStmt::BufferReset { span: s.clone() }], s);
        assert_eq!(block.body().len(), 1);
    }

    #[test]
    fn ir_cleanup_block_empty_body() {
        let block = IrCleanupBlock::new(vec![], test_span());
        assert!(block.body().is_empty());
    }
}