gbas 0.1.0

An assembler for Game Boy
Documentation
use super::{
    analyze_reloc_expr, CommandArgs, Directive, SemanticActions, SemanticAtom, SemanticExpr,
};
use backend;
use backend::{BinaryOperator, RelocAtom};
use diagnostics::{InternalDiagnostic, Message};
use expr::ExprVariant;
use frontend::session::Session;
use frontend::syntax::Literal;
use instruction::RelocExpr;
use span::Span;
use std::fmt::Debug;
use Width;

pub fn analyze_directive<'a, S: Session + 'a>(
    directive: (Directive, S::Span),
    args: CommandArgs<S>,
    actions: &mut SemanticActions<'a, S>,
) -> Result<(), InternalDiagnostic<S::Span>> {
    match directive.0 {
        Directive::Db => analyze_data(Width::Byte, args, actions),
        Directive::Ds => analyze_ds(directive.1, args, actions),
        Directive::Dw => analyze_data(Width::Word, args, actions),
        Directive::Include => analyze_include(directive.1, args, actions),
        Directive::Org => analyze_org(directive.1, args, actions),
    }
}

fn analyze_data<'a, S: Session + 'a>(
    width: Width,
    args: CommandArgs<S>,
    actions: &mut SemanticActions<'a, S>,
) -> Result<(), InternalDiagnostic<S::Span>> {
    for arg in args {
        let expr = analyze_reloc_expr(arg, &mut actions.expr_factory)?;
        actions.session.emit_item(backend::Item::Data(expr, width))
    }
    Ok(())
}

fn analyze_ds<'a, S: Session + 'a>(
    span: S::Span,
    args: CommandArgs<S>,
    actions: &mut SemanticActions<'a, S>,
) -> Result<(), InternalDiagnostic<S::Span>> {
    let arg = single_arg(span, args)?;
    let count = analyze_reloc_expr(arg, &mut actions.expr_factory)?;
    actions
        .session
        .set_origin(location_counter_plus_expr(count));
    Ok(())
}

fn location_counter_plus_expr<S: Clone>(expr: RelocExpr<S>) -> RelocExpr<S> {
    let span = expr.span.clone();
    RelocExpr {
        variant: ExprVariant::Binary(
            BinaryOperator::Plus,
            Box::new(RelocExpr {
                variant: ExprVariant::Atom(RelocAtom::LocationCounter),
                span: span.clone(),
            }),
            Box::new(expr),
        ),
        span,
    }
}

fn analyze_include<'a, F: Session + 'a>(
    span: F::Span,
    args: CommandArgs<F>,
    actions: &mut SemanticActions<'a, F>,
) -> Result<(), InternalDiagnostic<F::Span>> {
    let (path, span) = reduce_include(span, args)?;
    actions
        .session
        .analyze_file(path)
        .map_err(|err| InternalDiagnostic::new(err.into(), span))
}

fn reduce_include<I, S>(
    span: S,
    args: Vec<SemanticExpr<I, S>>,
) -> Result<(I, S), InternalDiagnostic<S>>
where
    I: Debug + PartialEq,
    S: Span,
{
    let arg = single_arg(span, args)?;
    match arg.variant {
        ExprVariant::Atom(SemanticAtom::Literal(Literal::String(path))) => Ok((path, arg.span)),
        _ => Err(InternalDiagnostic::new(Message::ExpectedString, arg.span)),
    }
}

fn analyze_org<'a, S: Session + 'a>(
    span: S::Span,
    args: CommandArgs<S>,
    actions: &mut SemanticActions<'a, S>,
) -> Result<(), InternalDiagnostic<S::Span>> {
    let arg = single_arg(span, args)?;
    let expr = analyze_reloc_expr(arg, &mut actions.expr_factory)?;
    actions.session.set_origin(expr);
    Ok(())
}

fn single_arg<T: Debug + PartialEq, S>(
    span: S,
    args: impl IntoIterator<Item = T>,
) -> Result<T, InternalDiagnostic<S>> {
    let mut args = args.into_iter();
    let arg = args.next().ok_or_else(|| {
        InternalDiagnostic::new(
            Message::OperandCount {
                actual: 0,
                expected: 1,
            },
            span,
        )
    })?;
    assert_eq!(args.next(), None);
    Ok(arg)
}

#[cfg(test)]
mod tests {
    use super::*;
    use codebase::CodebaseError;
    use frontend::semantics;
    use frontend::semantics::tests::*;
    use frontend::syntax::keyword::{Command, Operand};
    use frontend::syntax::{CommandContext, ExprAtom, ExprContext, FileContext, StmtContext};
    use std::borrow::Borrow;
    use std::io;

    #[test]
    fn build_include_item() {
        let filename = "file.asm";
        let actions = unary_directive(Directive::Include, |arg| {
            arg.push_atom((ExprAtom::Literal(Literal::String(filename.to_string())), ()));
        });
        assert_eq!(actions, [TestOperation::AnalyzeFile(filename.to_string())])
    }

    #[test]
    fn set_origin() {
        let origin = 0x3000;
        let actions = unary_directive(Directive::Org, |arg| arg.push_atom(mk_literal(origin)));
        assert_eq!(actions, [TestOperation::SetOrigin(origin.into())])
    }

    #[test]
    fn emit_byte_items() {
        test_data_items_emission(Directive::Db, mk_byte, [0x42, 0x78])
    }

    #[test]
    fn emit_word_items() {
        test_data_items_emission(Directive::Dw, mk_word, [0x4332, 0x780f])
    }

    fn mk_byte(byte: &i32) -> backend::Item<()> {
        backend::Item::Data((*byte).into(), Width::Byte)
    }

    fn mk_word(word: &i32) -> backend::Item<()> {
        backend::Item::Data((*word).into(), Width::Word)
    }

    fn test_data_items_emission(
        directive: Directive,
        mk_item: impl Fn(&i32) -> backend::Item<()>,
        data: impl Borrow<[i32]>,
    ) {
        let actions = with_directive(directive, |mut command| {
            for datum in data.borrow().iter() {
                let mut arg = command.add_argument();
                arg.push_atom(mk_literal(*datum));
                command = arg.exit();
            }
            command
        });
        assert_eq!(
            actions,
            data.borrow()
                .iter()
                .map(mk_item)
                .map(TestOperation::EmitItem)
                .collect::<Vec<_>>()
        )
    }

    #[test]
    fn reserve_3_bytes() {
        let actions = ds(|arg| arg.push_atom(mk_literal(3)));
        assert_eq!(
            actions,
            [TestOperation::SetOrigin(
                ExprVariant::Binary(
                    BinaryOperator::Plus,
                    Box::new(RelocAtom::LocationCounter.into()),
                    Box::new(3.into()),
                ).into()
            )]
        )
    }

    fn mk_literal(n: i32) -> (ExprAtom<String, Literal<String>>, ()) {
        (ExprAtom::Literal(Literal::Number(n)), ())
    }

    #[test]
    fn ds_with_malformed_expr() {
        let actions =
            ds(|arg| arg.push_atom((ExprAtom::Literal(Literal::Operand(Operand::A)), ())));
        assert_eq!(
            actions,
            [TestOperation::EmitDiagnostic(InternalDiagnostic::new(
                Message::KeywordInExpr { keyword: () },
                (),
            ))]
        )
    }

    #[test]
    fn ds_without_args() {
        test_unary_directive_without_args(Directive::Ds)
    }

    #[test]
    fn org_without_args() {
        test_unary_directive_without_args(Directive::Org)
    }

    #[test]
    fn include_without_args() {
        test_unary_directive_without_args(Directive::Include)
    }

    #[test]
    fn include_with_number() {
        let actions = unary_directive(Directive::Include, |arg| arg.push_atom(mk_literal(7)));
        assert_eq!(
            actions,
            [TestOperation::EmitDiagnostic(InternalDiagnostic::new(
                Message::ExpectedString,
                (),
            ))]
        )
    }

    #[test]
    fn data_with_malformed_expr() {
        let actions = unary_directive(Directive::Db, |arg| {
            arg.push_atom((ExprAtom::Literal(Literal::Operand(Operand::A)), ()))
        });
        assert_eq!(
            actions,
            [TestOperation::EmitDiagnostic(InternalDiagnostic::new(
                Message::KeywordInExpr { keyword: () },
                (),
            ))]
        )
    }

    #[test]
    fn include_file_with_invalid_utf8() {
        let name = "invalid_utf8.s";
        let mut frontend = TestFrontend::new();
        frontend.fail(CodebaseError::Utf8Error);
        {
            let mut context = SemanticActions::new(&mut frontend)
                .enter_stmt(None)
                .enter_command((Command::Directive(Directive::Include), ()))
                .add_argument();
            context.push_atom((ExprAtom::Literal(Literal::String(name.into())), ()));
            context.exit().exit().exit();
        }
        assert_eq!(
            frontend.into_inner(),
            [
                TestOperation::AnalyzeFile(name.into()),
                TestOperation::EmitDiagnostic(InternalDiagnostic::new(Message::InvalidUtf8, ()))
            ]
        )
    }

    #[test]
    fn include_nonexistent_file() {
        let name = "nonexistent.s";
        let message = "some message";
        let mut frontend = TestFrontend::new();
        frontend.fail(CodebaseError::IoError(io::Error::new(
            io::ErrorKind::NotFound,
            message,
        )));
        {
            let mut context = SemanticActions::new(&mut frontend)
                .enter_stmt(None)
                .enter_command((Command::Directive(Directive::Include), ()))
                .add_argument();
            context.push_atom((ExprAtom::Literal(Literal::String(name.into())), ()));
            context.exit().exit().exit();
        }
        assert_eq!(
            frontend.into_inner(),
            [
                TestOperation::AnalyzeFile(name.into()),
                TestOperation::EmitDiagnostic(InternalDiagnostic::new(
                    Message::IoError {
                        string: message.to_string()
                    },
                    ()
                ))
            ]
        )
    }

    fn ds(
        f: impl for<'a> FnOnce(&mut semantics::ExprContext<'a, TestFrontend>),
    ) -> Vec<TestOperation> {
        unary_directive(Directive::Ds, f)
    }

    fn unary_directive<F>(directive: Directive, f: F) -> Vec<TestOperation>
    where
        F: for<'a> FnOnce(&mut semantics::ExprContext<'a, TestFrontend>),
    {
        with_directive(directive, |command| {
            let mut arg = command.add_argument();
            f(&mut arg);
            arg.exit()
        })
    }

    fn test_unary_directive_without_args(directive: Directive) {
        let actions = with_directive(directive, |command| command);
        assert_eq!(
            actions,
            [TestOperation::EmitDiagnostic(InternalDiagnostic::new(
                Message::OperandCount {
                    actual: 0,
                    expected: 1
                },
                (),
            ))]
        )
    }

    fn with_directive<F>(directive: Directive, f: F) -> Vec<TestOperation>
    where
        F: for<'a> FnOnce(semantics::CommandActions<'a, TestFrontend>)
            -> semantics::CommandActions<'a, TestFrontend>,
    {
        collect_semantic_actions(|actions| {
            let command = actions
                .enter_stmt(None)
                .enter_command((Command::Directive(directive), ()));
            f(command).exit().exit()
        })
    }
}