use crate::mapping::{get_mapping_configuration, Mapping};
use crate::node::{
    is_asan_friendly_name, CommentType, ControlType, EchoKind, NodeType, OperationType, PNode,
    PString,
};
use crate::object::{Bundle, Context, Object, ObjectType, GLOBAL_CONTEXT};
use crate::opcodes::{AddressingMode, INSTRUCTIONS};
use crate::parser::Parser;
use crate::SourceInfo;
use crate::{Error, ExpandedFrom};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Neg;
use std::ops::Range;

/// The mode in which a literal is expressed.
#[derive(Clone, PartialEq)]
enum LiteralMode {
    /// An 8/16 bit hexadecimal value.
    Hexadecimal,

    /// A byte expressed in binary format.
    Binary,

    /// A byte expressed in plain decimal format.
    Plain,
}

/// The different stages that the assembler goes through and which are relevant
/// for the process.
#[derive(Debug, PartialEq)]
enum Stage {
    /// The context is still building up (i.e. we don't have all the variable
    /// values, labels and their addresses yet).
    Context,

    /// An initial context is there, and we can already consume most bundles. We
    /// only need to leave some of them as pending if they require a value that
    /// is relative to the end size of segments.
    Bundling,

    /// We have most bundles, and we already know the size for all segments.
    /// Hence, we can resolve (crunch) the nodes that were pending to be
    /// bundled.
    Crunching,
}

/// A Node which is pending to be resolved, with all the info necessary so a
/// stage like Stage::Crunching can re-create the original context.
#[derive(Clone, Debug)]
struct PendingNode {
    mapping: usize,
    segment: usize,
    context: String,
    bundle_index: usize,
    node: PNode,
    labels_seen: usize,
    macro_context: Vec<ExpandedFrom>,
    pending_defines: Option<Vec<PendingDefine>>,
}

/// A definition that is pending to be re-created whenever we crunch for
/// PendingNode's.
#[derive(Clone, Debug)]
struct PendingDefine {
    id: PString,
    obj: Object,
}

/// Memory range that can be identified by a name.
#[derive(Clone, Debug, PartialEq)]
pub struct MemoryRange {
    // The range in memory itself.
    pub range: Range<usize>,

    // Name that can access this memory range.
    pub name: String,
}

impl MemoryRange {
    /// Display the range in hexadecimal format and by taking into consideration
    /// on whether it's really a range or a single value.
    pub fn range_to_human(&self) -> String {
        if self.range.start + 1 == self.range.end {
            if self.range.start <= 0xFF {
                return format!("${:02X}", self.range.start);
            }
            return format!("${:04X}", self.range.start);
        }

        if self.range.start <= 0xFF {
            return format!("${:02X}-${:02X}", self.range.start, self.range.end - 1);
        }
        format!("${:04X}-${:04X}", self.range.start, self.range.end - 1)
    }
}

impl std::fmt::Display for MemoryRange {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "'{}' ({})", self.name, self.range_to_human())
    }
}

struct Assembler<'a> {
    context: Context,
    literal_mode: Option<LiteralMode>,
    stage: Stage,
    macros: HashMap<String, &'a PNode>,
    mappings: Vec<Mapping>,
    current_mapping: usize,
    current_segment: usize,
    pending: Vec<PendingNode>,
    labels_seen: usize,

    // Number of macro statements seen on a given iteration. Note that the
    // parser guarantees that macros are defined well (no unclosed macros nor
    // too many .endmacro's).
    macros_seen: usize,

    // Same as macros_seen but for .proc's.
    procs_seen: usize,

    // Same as macros_seen but for .repeat's.
    repeats_seen: usize,

    // Warnings that have accumulated over the run.
    warnings: Vec<Error>,

    // Sources that have been evaluated for the current session. This is
    // directly tied to `Parser::sources`.
    sources: Vec<SourceInfo>,

    // Whether the assembler has detected accesses to Working RAM or not
    // (0x6000-0x7FFF).
    accessing_working_ram: bool,

    // Whether the Address Sanitizer is enabled or not.
    asan_enabled: bool,

    // Whether the next assignment should be ignored or not by the address
    // sanitizer.
    asan_next_ignore: bool,

    // The amount of bytes to reserve for the next variable assignment.
    asan_next_reserve: usize,

    // The memory range as defined by an 'asan:stack' statement.
    asan_stack_definition: Option<MemoryRange>,

    // Stack of macro expansions. That is, every time a macro is expanded
    // (i.e. via 'bundle_call'), the node that performed the expansion is
    // stacked here. This is then picked up via structs like 'PendingNode' and
    // 'Error' so the programmer gets information on all the macro expansions
    // that happened before reaching a given error.
    macro_context: Vec<ExpandedFrom>,

    // Stack of pending definitions. The last element from this list is the list
    // to be set to the next PendingNode push.
    pending_defines: Vec<Vec<PendingDefine>>,
}

/// The result to be given at the end of `assembler::assemble` and
/// `assembler::assemble_with_mapping` functions. It includes everything a
/// caller might want to know about the resulting memory layout.
#[derive(Debug, Default)]
pub struct MemoryResult {
    /// Memory ranges used by the source.
    pub memory_ranges: Vec<MemoryRange>,

    /// Total Internal RAM being used by the source in bytes.
    pub total_internal_ram: usize,

    /// Total Working RAM being used by the source in bytes. Ignore this field
    /// if `accessing_working_ram` has been set to false in `Assembler`.
    pub total_working_ram: usize,
}

/// All the information that a caller needs after calling either
/// `assembler::assemble` or `assembler::assemble_with_mapping`.
#[derive(Debug)]
pub struct AssemblerResult {
    /// The binary data that comes as a result from assembling a given
    /// source. Ignore this if `errors` is not empty.
    pub bundles: Vec<Bundle>,

    /// Errors which the assembler detected and which should make the
    /// application to fail.
    pub errors: Vec<Error>,

    /// Issues caught by the assembler which is up to the application to deem as
    /// a failure or not.
    pub warnings: Vec<Error>,

    /// The resulting mappings after assembling a source. You can count on
    /// fields like `offset` if `errors` is empty.
    pub mappings: Vec<Mapping>,

    /// Whether the resulting ROM needs Working RAM to be available in order to
    /// work. Ignore this field if `errors` is not empty.
    pub accessing_working_ram: bool,

    /// The resaulting memory analysis from the address sanitizer. Ignore this
    /// if you did not assemble the source with the address sanitizer enabled.
    pub memory: MemoryResult,
}

/// Read the contents from the `reader` as a source file and produce a list of
/// bundles that can be formatted as binary data. You also need to pass
/// information of the source by means of `source`, as otherwise control
/// statements like ".import" or ".incbin" wouldn't know how to resolve relative
/// paths. You can specify the mapper to be used as an identifier in `mapping`,
/// which will be handled via `get_mapping_configuration`.
pub fn assemble(
    reader: impl Read,
    mapping: &str,
    defines: &[(String, u8)],
    source: &SourceInfo,
    asan: bool,
) -> AssemblerResult {
    let config = match get_mapping_configuration(mapping) {
        Ok(config) => config,
        Err(e) => {
            return AssemblerResult {
                bundles: vec![],
                errors: vec![Error {
                    global: true,
                    line: 0,
                    message: e,
                    source: source.clone(),
                    expanded_from: vec![],
                }],
                warnings: vec![],
                mappings: vec![],
                accessing_working_ram: false,
                memory: MemoryResult::default(),
            };
        }
    };

    assemble_with_mapping(reader, config, defines, source, asan)
}

/// Read the contents from the `reader` as a source file and produce a list of
/// bundles that can be formatted as binary data. You also need to pass
/// information of the source by means of `source`, as otherwise control
/// statements like ".import" or ".incbin" wouldn't know how to resolve relative
/// paths. You also need to provide the `mapping` as handled internally. If you
/// are unsure how to use it, just call `assemble`.
pub fn assemble_with_mapping(
    reader: impl Read,
    mapping: Vec<Mapping>,
    defines: &[(String, u8)],
    source: &SourceInfo,
    asan: bool,
) -> AssemblerResult {
    // Save the original current working directory in case it changes after a
    // directive like '.incbin' or '.include'.
    let original_directory = std::env::current_dir().unwrap();

    let mut asm = Assembler::new(mapping);
    let mut memory = MemoryResult::default();
    asm.asan_enabled = asan;

    // First of all, parse the input so we get a list of nodes we can work
    // with.
    let mut parser = Parser::default();
    if let Err(errors) = parser.parse(reader, source) {
        return AssemblerResult {
            bundles: vec![],
            errors,
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        };
    }

    let nodes = parser.nodes();
    asm.sources = parser.sources;

    // Before building a context, add variables if they were defined by the
    // caller.
    for (name, value) in defines {
        if let Err(e) = asm.define_variable_value(name, *value) {
            return AssemblerResult {
                bundles: vec![],
                errors: e.into(),
                warnings: vec![],
                mappings: asm.mappings,
                accessing_working_ram: asm.accessing_working_ram,
                memory,
            };
        }
    }

    // Build the context by iterating over the parsed nodes and checking
    // where scopes start/end, evaluating values for variables, labels, etc.
    if let Err(errors) = asm.eval_context(&nodes) {
        return AssemblerResult {
            bundles: vec![],
            errors,
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        };
    }

    // We have enough information for asan:stack after evaluating the
    // context. So, if asan is enabled, account for it now, otherwise push a
    // warning.
    if asm.asan_enabled {
        match asm.asan_stack_definition {
            Some(ref mr) => {
                memory.memory_ranges.push(mr.clone());
                memory.total_internal_ram += mr.range.len();
            }
            None => {
                asm.warnings.push(Error {
                    line: 0,
                    message: "you have not defined a range for 'asan:stack'".to_string(),
                    source: source.clone(),
                    global: true,
                    expanded_from: vec![],
                });
            }
        }
    }

    // Convert the relevant nodes into binary bundles which can be used by
    // the caller. This is done for most nodes, even if some of them will
    // have to be marked as pending, since they depend on knowing the exact
    // size for a given segment.
    if let Err(errors) = asm.bundle(&nodes) {
        return AssemblerResult {
            bundles: vec![],
            errors,
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        };
    }

    // Now we know how much each segment spans, and we can resolve (crunch)
    // the nodes marked as pending.
    if let Err(errors) = asm.crunch() {
        return AssemblerResult {
            bundles: vec![],
            errors,
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        };
    }

    if asm.asan_enabled {
        if let Err(errors) = asm.asan(&mut memory) {
            return AssemblerResult {
                bundles: vec![],
                errors,
                warnings: asm.warnings,
                mappings: asm.mappings,
                accessing_working_ram: asm.accessing_working_ram,
                memory,
            };
        }
    }

    // Return back to the original directory for this session. This is important
    // so after calling this function the environment is not completely borked,
    // but it's not so important that in (the very unlikely) case that the call
    // fails we have to bail out.
    let _ = std::env::set_current_dir(original_directory);

    // All set, fill the vector of bundles to be returned.
    match asm.fill() {
        Ok(bundles) => AssemblerResult {
            bundles,
            errors: vec![],
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        },
        Err(errors) => AssemblerResult {
            bundles: vec![],
            errors,
            warnings: asm.warnings,
            mappings: asm.mappings,
            accessing_working_ram: asm.accessing_working_ram,
            memory,
        },
    }
}

impl<'a> Assembler<'a> {
    fn new(mappings: Vec<Mapping>) -> Self {
        Self {
            context: Context::new(),
            literal_mode: None,
            stage: Stage::Context,
            macros: HashMap::new(),
            mappings,
            current_mapping: 0,
            current_segment: 0,
            pending: vec![],
            labels_seen: 0,
            macros_seen: 0,
            procs_seen: 0,
            repeats_seen: 0,
            warnings: vec![],
            sources: vec![],
            accessing_working_ram: false,
            asan_enabled: false,
            asan_next_ignore: false,
            asan_next_reserve: 1,
            asan_stack_definition: None,
            macro_context: vec![],
            pending_defines: vec![],
        }
    }

    /// Define a new variable by using the given `name` and `value`.
    fn define_variable_value(&mut self, name: &String, value: u8) -> Result<(), Error> {
        if name.is_empty() {
            return Err(Error {
                global: true,
                line: 0,
                source: self.sources.first().unwrap().clone(),
                expanded_from: self.macro_context.clone(),
                message: "empty variable name".to_string(),
            });
        }

        let var_name = PString {
            value: name.to_string(),
            line: 0,
            start: 0,
            end: name.len(),
        };
        let var_value = Object {
            bundle: Bundle::fill(value),
            node: None,
            mapping: self.current_mapping,
            segment: self.current_segment,
            object_type: ObjectType::Value,
            asan_ignore: false,
            asan_reserve: 1,
            accessed: 0,
        };

        if let Err(err) = self.context.set_variable(&var_name, &var_value, false) {
            Err(Error {
                global: true,
                line: 0,
                source: self.sources.first().unwrap().clone(),
                expanded_from: self.macro_context.clone(),
                message: err,
            })
        } else {
            Ok(())
        }
    }

    // Define a new variable by taking the given `id`. This variable will only
    // be created if `id` is not empty. The function will error out if the given
    // name is already taken.
    fn define_variable(&mut self, node: &PNode) -> Result<(), Error> {
        if node.value.is_empty() {
            return Ok(());
        }

        if let Err(message) = self.context.set_variable(
            &node.value,
            &Object::new(
                self.current_mapping,
                self.current_segment,
                ObjectType::Address,
            ),
            false,
        ) {
            return Err(Error {
                message,
                line: node.value.line,
                global: false,
                expanded_from: self.macro_context.clone(),
                source: self.source_for(node),
            });
        }

        Ok(())
    }

    fn eval_context(&mut self, nodes: &'a [PNode]) -> Result<(), Vec<Error>> {
        let mut errors = Vec::new();

        for node in nodes {
            // Remove asan:ignore if the current node is not to be an assignment.
            if self.asan_next_ignore && !matches!(&node.node_type, NodeType::Assignment) {
                self.asan_next_ignore = false;
                self.asan_next_reserve = 1;
            }

            match &node.node_type {
                // At this stage we only define the label into the current
                // context so it's known. The actual value cannot be computed
                // right now as we don't know the segment size where it belongs
                // yet.
                NodeType::Label => {
                    // There's no good reason to declare a named label inside of
                    // a macro. If that's the case, just error out.
                    if (self.macros_seen > 0 || self.repeats_seen > 0) && !node.value.is_empty() {
                        errors.push(Error {
                            line: node.value.line,
                            message: format!(
                                "using a named label ('{}') inside of a macro/repeat definition",
                                node.value.value
                            ),
                            source: self.source_for(node),
                            expanded_from: self.macro_context.clone(),
                            global: false,
                        });
                        continue;
                    }
                    if let Err(err) = self.define_variable(node) {
                        errors.push(err);
                    }
                }
                NodeType::Comment(CommentType::AsanReserve(size)) => {
                    self.asan_next_reserve = *size;
                }
                NodeType::Comment(CommentType::AsanReserveIdentifier(id)) => {
                    match self.context.get_variable(id, &self.mappings) {
                        Ok(var) => self.asan_next_reserve = var.bundle.value() as usize,
                        Err(_) => {
                            errors.push(Error {
                                line: node.value.line,
                                message: format!(
                                    "could not reserve for unknown value of '{}'",
                                    id.value,
                                ),
                                source: self.source_for(node),
                                expanded_from: self.macro_context.clone(),
                                global: false,
                            });
                        }
                    }
                }
                NodeType::Comment(CommentType::AsanStack(range)) => {
                    match self.asan_stack_definition {
                        Some(_) => {
                            if self.asan_stack_definition.is_some() {
                                errors.push(Error {
                                    message: "you have already defined a value for 'asan:stack'"
                                        .to_string(),
                                    line: node.value.line,
                                    source: self.source_for(node),
                                    expanded_from: self.macro_context.clone(),
                                    global: false,
                                });
                            }
                        }
                        None => {
                            self.asan_stack_definition = Some(MemoryRange {
                                range: range.clone(),
                                name: "<stack>".to_string(),
                            });
                        }
                    }
                }
                NodeType::Comment(CommentType::AsanIgnore) => {
                    self.asan_next_ignore = true;
                }
                NodeType::Assignment => {
                    if self.macros_seen > 0 || self.repeats_seen > 0 {
                        errors.push(Error {
                            message: "cannot have assignments inside of macro/repeat definitions"
                                .to_string(),
                            line: node.value.line,
                            source: self.source_for(node),
                            expanded_from: self.macro_context.clone(),
                            global: false,
                        });
                        continue;
                    }
                    self.literal_mode = None;

                    match self.evaluate_node(node.left.as_ref().unwrap()) {
                        Ok(value) => {
                            // Check that the given name makes sense if the
                            // address sanitizer is enabled and the assigned
                            // value is known.
                            if self.asan_enabled && value.resolved {
                                if let Err(err) = self.asan_check_variable_name(node, &value) {
                                    errors.push(err);
                                }
                            }

                            if let Err(err) = self.context.set_variable(
                                &node.value,
                                &Object {
                                    bundle: value,
                                    node: None,
                                    mapping: self.current_mapping,
                                    segment: self.current_segment,
                                    object_type: ObjectType::Value,
                                    asan_ignore: self.asan_next_ignore,
                                    asan_reserve: self.asan_next_reserve,
                                    accessed: 0,
                                },
                                false,
                            ) {
                                errors.push(Error {
                                    message: err,
                                    line: node.value.line,
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                    source: self.source_for(node),
                                });
                            }
                            self.asan_next_ignore = false;
                            self.asan_next_reserve = 1;
                        }
                        Err(e) => errors.push(e),
                    }
                }
                NodeType::Control(control_type) => {
                    if !self.context.is_global() && control_type.must_be_global() {
                        errors.push(Error {
                            message: format!("{control_type} must be on the global scope"),
                            line: node.value.line,
                            global: false,
                            expanded_from: self.macro_context.clone(),
                            source: self.source_for(node),
                        });
                        continue;
                    }

                    match control_type {
                        ControlType::StartMacro => {
                            // NOTE: macros inside of macros cannot happen
                            // because of the previous check of them needing to
                            // be on the global scope. Hence, we don't have to
                            // do a similar check as for .proc's or .scope's on
                            // illegal definitions.
                            self.macros_seen += 1;

                            let name = &node.left.as_ref().unwrap().value;
                            if let Err(e) = name.is_valid_identifier(false) {
                                errors.push(Error {
                                    message: format!(
                                        "'{}' is not a valid macro name: {e}",
                                        name.value
                                    ),
                                    line: node.value.line,
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                    source: self.source_for(node),
                                });
                                continue;
                            } else {
                                // Insert a reference to this node so it can be
                                // unrolled whenever we have to perform a macro
                                // call.
                                self.macros.entry(name.value.clone()).or_insert(node);
                            }
                        }
                        ControlType::EndMacro
                            if self.macros_seen > 0 => {
                                self.macros_seen -= 1;
                            }
                        // Same as NodeType::Label.
                        ControlType::StartProc => {
                            if self.macros_seen > 0 || self.procs_seen > 0 || self.repeats_seen > 0
                            {
                                errors.push(Error {
                                    message: "you cannot call '.proc' in this context".to_string(),
                                    line: node.value.line,
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                    source: self.source_for(node),
                                });
                                continue;
                            }

                            self.procs_seen += 1;
                            let proc_name = &node.left.as_ref().unwrap();
                            if let Err(err) = proc_name.value.is_valid_identifier(false) {
                                errors.push(Error {
                                    message: format!(
                                        "'{}' is not a valid proc name: {err}",
                                        proc_name.value.value,
                                    ),
                                    line: node.value.line,
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                    source: self.source_for(node),
                                });
                                continue;
                            } else if let Err(err) = self.define_variable(proc_name) {
                                errors.push(err);
                            }
                        }
                        ControlType::EndProc
                            // Same case as with macros.
                            if self.procs_seen > 0 => {
                                self.procs_seen -= 1;
                            }
                        ControlType::StartScope => {
                            if self.macros_seen > 0 || self.procs_seen > 0 || self.repeats_seen > 0
                            {
                                errors.push(Error {
                                    message: "you cannot call '.scope' in this context".to_string(),
                                    line: node.value.line,
                                    source: self.source_for(node),
                                    expanded_from: self.macro_context.clone(),
                                    global: false,
                                });
                                continue;
                            }
                            let scope_name = &node.left.as_ref().unwrap();
                            if let Err(err) = scope_name.value.is_valid_identifier(false) {
                                errors.push(Error {
                                    message: format!(
                                        "'{}' is not a valid scope name: {err}",
                                        scope_name.value.value,
                                    ),
                                    line: node.value.line,
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                    source: self.source_for(node),
                                });
                                continue;
                            }
                        }
                        ControlType::StartRepeat => {
                            self.repeats_seen += 1;
                        }
                        ControlType::EndRepeat
                            if self.repeats_seen > 0 => {
                                self.repeats_seen -= 1;
                            }
                        _ => {}
                    }

                    // If this control statement implies a context change, do it
                    // now.
                    if let Err(message) = self.context.change_context(node) {
                        errors.push(Error {
                            message,
                            line: node.value.line,
                            global: false,
                            expanded_from: self.macro_context.clone(),
                            source: self.source_for(node),
                        });
                    }

                    // If this control statement actually has a body, try to go
                    // inside of it. That is not possible, for example, on
                    // if/ifdef/ifndef conditions with a condition that
                    // evaluates to false.
                    if control_type.has_body() {
                        match control_type {
                            ControlType::IfDef | ControlType::IfNDef => {
                                // NOTE: you cannot realistically evaluate a
                                // condition which might depend on macro
                                // arguments which have not been provided at
                                // this stage. Hence, don't go into the inner
                                // block if this is the case.
                                if self.macros_seen == 0 {
                                    self.evaluate_ifdef_block(node)?
                                }
                            }
                            ControlType::If => {
                                // NOTE: see IfDef | IfNDef.
                                if self.macros_seen == 0 {
                                    self.evaluate_if_block(node)?;
                                }
                            }
                            _ => {
                                let inner = &node.right.as_ref().unwrap().args.as_ref().unwrap();
                                self.eval_context(inner)?;
                            }
                        };
                    }
                }
                _ => {}
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    // Apply the current segment offset to the label identified by `id` unless
    // it's empty (i.e. anonymous label). In either case, the computed label
    // will be pushed into the context's list of known labels with the current
    // segment offset.
    fn apply_segment_offset_to_label(&mut self, node: &PNode) -> Result<(), Error> {
        let segment = &self.mappings[self.current_mapping].segments[self.current_segment];
        let value = segment.offset.to_le_bytes();
        let object = Object {
            bundle: Bundle {
                bytes: [value[0], value[1], value[2]],
                size: 2,
                address: 0,
                cycles: 0,
                affected_on_page: false,
                resolved: false,
                negative: false,
            },
            node: None,
            mapping: self.current_mapping,
            segment: self.current_segment,
            object_type: ObjectType::Address,
            asan_ignore: false,
            asan_reserve: 1,
            accessed: 0,
        };

        if !node.value.is_empty() {
            if let Err(message) = self.context.set_variable(&node.value, &object, true) {
                return Err(Error {
                    message,
                    line: node.value.line,
                    global: false,
                    expanded_from: self.macro_context.clone(),
                    source: self.source_for(node),
                });
            }
        }
        self.context.add_label(&object);

        Ok(())
    }

    fn bundle(&mut self, nodes: &'a [PNode]) -> Result<(), Vec<Error>> {
        let mut errors = Vec::new();
        let mut next_ignore = false;

        self.stage = Stage::Bundling;

        for node in nodes {
            match &node.node_type {
                // Initialize the label to the offset address of the current
                // segment. Note that this is only the offset from the beginning
                // of the offset, the effective address will only be available
                // after calling `Context::get_variable`
                NodeType::Label => {
                    if let Err(e) = self.apply_segment_offset_to_label(node) {
                        errors.push(e);
                    }
                }
                // Same as with labels but with the addition that ".proc"
                // introduces a new context. Hence, first act as a label, and
                // then open up its inner context.
                NodeType::Control(ControlType::StartProc) => {
                    let proc_name = &node.left.as_ref().unwrap();
                    if let Err(e) = self.apply_segment_offset_to_label(proc_name) {
                        errors.push(e);
                    }
                    if let Err(message) = self.context.change_context(node) {
                        errors.push(Error {
                            message,
                            line: node.value.line,
                            global: false,
                            expanded_from: self.macro_context.clone(),
                            source: self.source_for(node),
                        });
                    }

                    // And now go inside of its body if it exists (note that its
                    // existence might not be guaranteed if the parser gave an
                    // error on this block).
                    // if node.right.as_ref().is_some() {
                    let args = &node.right.as_ref().unwrap().args.as_ref().unwrap();
                    if args.is_empty() {
                        self.warnings.push(Error {
                            line: node.value.line,
                            message: format!("empty .proc '{}'", proc_name.value.value),
                            source: self.source_for(node),
                            global: false,
                            expanded_from: self.macro_context.clone(),
                        });
                    } else {
                        self.bundle(args)?;
                    }
                }
                NodeType::Comment(CommentType::AsanIgnore) => {
                    next_ignore = true;
                    self.asan_next_ignore = true;
                }
                NodeType::Instruction => {
                    self.literal_mode = None;
                    match self.evaluate_node(node) {
                        Ok(bundle) => {
                            if let Err(e) = self.push_bundle(bundle, node) {
                                errors.push(e);
                            }
                        }
                        Err(e) => errors.push(e),
                    }
                }
                NodeType::Control(control_type) => {
                    if let Err(mut e) = self.evaluate_control_statement(node) {
                        errors.append(&mut e);
                    }

                    // On control statements which modify the context, there are
                    // some further evaluating to do.
                    match control_type {
                        ControlType::Echo(t) => self.evaluate_echo(node, t)?,
                        ControlType::StartRepeat => {
                            self.evaluate_repeat_statement(node)?;
                        }
                        ControlType::StartScope
                            // If this is the start of a .scope statement, then go
                            // inside of its body too if it exists (note that its
                            // existence might not be guaranteed if the parser gave an
                            // error on this block). Note that we do that too for
                            // .proc's in its specialized branch, and we don't want to
                            // do it for macros as they will be evaluated on a per call
                            // basis.
                            if node.right.as_ref().is_some() => {
                                let scope_name = &node.left.as_ref().unwrap().value;
                                let args = &node.right.as_ref().unwrap().args.as_ref().unwrap();
                                if args.is_empty() {
                                    self.warnings.push(Error {
                                        line: node.value.line,
                                        message: format!("empty .scope '{}'", scope_name.value),
                                        source: self.source_for(node),
                                        global: false,
                                        expanded_from: self.macro_context.clone(),
                                    });
                                } else {
                                    self.bundle(args)?;
                                }
                            }
                        _ => {}
                    }
                }
                NodeType::Value | NodeType::Call => {
                    if let Err(mut ers) = self.bundle_call(node) {
                        errors.append(&mut ers);
                    }
                }
                NodeType::Fallthrough => {
                    // We don't do much other than pushing a fake PendingNode with a
                    // 'bundle_index' value that will point to the next node.
                    let current = &mut self.mappings[self.current_mapping];
                    self.pending.push(PendingNode {
                        mapping: self.current_mapping,
                        segment: self.current_segment,
                        context: self.context.name().to_string(),
                        bundle_index: current.segments[self.current_segment].bundles.len(),
                        node: node.to_owned(),
                        labels_seen: self.context.labels_seen(),
                        macro_context: self.macro_context.clone(),
                        pending_defines: self.pending_defines.last().cloned(),
                    });
                }
                _ => {}
            }

            // Keep `self.asan_next_ignore` to false if possible.
            if next_ignore {
                next_ignore = false;
            } else {
                self.asan_next_ignore = false;
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    fn crunch(&mut self) -> Result<(), Vec<Error>> {
        let mut errors = vec![];

        self.stage = Stage::Crunching;

        for pn in self.pending.clone() {
            self.labels_seen = pn.labels_seen;
            self.context.force_context_switch(&pn.context);

            // __fallthrough__ is handled here, whenever we already know
            // addresses, sizes, etc. If this is the case, this is not a real
            // node that can be bundled, but perform its check and move into the
            // next iteration.
            //
            // NOTE: this has to happen with a context switch, otherwise the
            // name resolution won't be accurate.
            if matches!(pn.node.node_type, NodeType::Fallthrough) {
                if let Err(e) = self.fallthrough(&pn) {
                    errors.push(e);
                }
                continue;
            }

            // Re-create the definitions for this context which only made sense
            // locally. These are basically arguments from a macro call.
            if let Some(pd) = &pn.pending_defines {
                for d in pd {
                    // Does this definition have a node attach to it? If so,
                    // then evaluate it again but with all the gained context we
                    // have at this stage. This way we will resolve the proper
                    // value for it even if it involved an arithmetical
                    // operation with an address we couldn't resolve before this
                    // stage.
                    let _ = match &d.obj.node {
                        Some(n) => {
                            // Yes! We have to evaluate this node again.
                            self.literal_mode = None;
                            let o = self.evaluate_node(n)?;

                            // Re-create the bundle to be passed from this
                            // object member, and set it as the variable's
                            // value.
                            let mut dobj = d.obj.clone();
                            dobj.bundle = o;
                            self.context.set_variable(&d.id, &dobj, true)
                        }
                        // No attached node, we can proceed by setting the
                        // variable as is.
                        None => self.context.set_variable(&d.id, &d.obj, true),
                    };
                }
            }

            self.literal_mode = None;
            match self.evaluate_node(&pn.node) {
                Ok(mut bundle) => {
                    let current = &self.mappings[pn.mapping].segments[pn.segment];
                    bundle.address = current.bundles[pn.bundle_index].address;

                    // If we are trying to 'jmp'/'jsr' right into the next
                    // instruction, then warn the programmer about it. This
                    // looks silly but in practice it might happen inside of a
                    // .proc where the code layout might not make this as
                    // obvious as it sounds.
                    //
                    // NOTE: this is only done for absolute addressing as the
                    // indirect case for 'jmp' is harder to follow and just not
                    // worth it. If programmers do fancy indirect jumps, let
                    // them shoot themselves in the foot if that's what they
                    // want.
                    if (bundle.bytes[0] == 0x4C || bundle.bytes[0] == 0x20)
                        && bundle.arg() as usize == bundle.next_address()
                    {
                        self.warnings.push(Error {
                            line: pn.node.value.line,
                            message: String::from(
                                "unconditional jump that points to the next instruction",
                            ),
                            source: self.source_for(&pn.node),
                            global: false,
                            expanded_from: pn.macro_context.clone(),
                        });
                    }

                    if pn.node.is_branch() {
                        bundle.resolved = true;
                        if let Err(e) = self.to_relative_address(&pn.node, &mut bundle) {
                            errors.push(e);
                        }

                        // Silly mistake coming from using a label directly
                        // after the current branch instruction.
                        if bundle.bytes[1] == 0 {
                            self.warnings.push(Error {
                                line: pn.node.value.line,
                                message: String::from(
                                    "conditional jump that points to the next instruction",
                                ),
                                source: self.source_for(&pn.node),
                                expanded_from: pn.macro_context.clone(),
                                global: false,
                            });
                        }
                    }

                    let current_mut = &mut self.mappings[pn.mapping].segments[pn.segment];
                    current_mut.bundles[pn.bundle_index].bytes = bundle.bytes;
                }
                Err(e) => errors.push(e),
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    fn fallthrough(&mut self, pn: &PendingNode) -> Result<(), Error> {
        // If we are not in 'crunch' mode, then it's a bug.
        assert_eq!(self.stage, Stage::Crunching);

        // If there was no "argument", then skip things altogether. The
        // programmer opted for an explicit fallthrough without further checks.
        if pn.node.value.is_empty() {
            return Ok(());
        }

        // If there is an argument, it has to be a valid address identifier.
        if let Err(message) = pn.node.value.is_valid_identifier(true) {
            return Err(Error {
                line: pn.node.value.line,
                message,
                source: self.source_for(&pn.node),
                expanded_from: pn.macro_context.clone(),
                global: false,
            });
        }

        // The check looks scarier than it is. We first grab the target address
        // by evaluating the identifier as a variable. This should just gives as
        // the address as is by calling .value(). The effective address is taken
        // from the 'bundle_index' from the PendingNode, as it was pushed while
        // pointing to the "next" bundle. Hence, we just fetch that bundle and
        // get its address.
        let target_address = self.evaluate_variable(&pn.node)?.value() as usize;
        let current = &self.mappings[pn.mapping].segments[pn.segment];
        let Some(effective) = current.bundles.get(pn.bundle_index) else {
            return Err(Error {
                line: pn.node.value.line,
                message: String::from("statement does not fall through"),
                source: self.source_for(&pn.node),
                expanded_from: pn.macro_context.clone(),
                global: false,
            });
        };

        if effective.address != target_address {
            return Err(Error {
                line: pn.node.value.line,
                message: format!(
                    "statement expects to progress on '{}' (${:04X}), but the next address is ${:04X}",
                    pn.node.value.value, target_address, effective.address,
                ),
                source: self.source_for(&pn.node),
                expanded_from: pn.macro_context.clone(),
                global: false,
            });
        }
        Ok(())
    }

    fn asan(&mut self, memory: &mut MemoryResult) -> Result<(), Vec<Error>> {
        let mut errors = vec![];

        for (context_name, map) in self.context.map.iter() {
            for (name, bundle) in map {
                // Ignore this bundle if the user explicitely told us to do so.
                if bundle.asan_ignore {
                    continue;
                }

                // If it doesn't have the proper prefix, skip as well.
                if !is_asan_friendly_name(name) {
                    continue;
                }
                let actual_name = name.split("::").last().unwrap_or("");

                // Build up the memory range object for this bundle.
                let val = bundle.bundle.value() as usize;
                let range = MemoryRange {
                    range: (val..val + bundle.asan_reserve),
                    name: if context_name == GLOBAL_CONTEXT {
                        name.clone()
                    } else {
                        format!("{context_name}::{name}")
                    },
                };

                // Check if this variable was ever accessed and warn about
                // it. Even if later it conflicts with another memory range, the
                // fact that it's not used is not a danger and hence a conflict
                // does not have to be reported. Hence, skip after issueing the
                // warning.
                if bundle.accessed == 0 {
                    self.warnings.push(Error {
                        line: 0,
                        message: format!("variable {range} is unused"),
                        source: self.sources[0].clone(),
                        expanded_from: self.macro_context.clone(),
                        global: true,
                    });
                    continue;
                }

                // Increase the counters for memory usage on either RAM slot and
                // check for bounds.
                if actual_name.starts_with("zp_") || actual_name.starts_with("m_") {
                    // Avoid considering references to PPU/APU addresses as
                    // reserved memory.
                    if range.range.start < 0x2000 {
                        memory.total_internal_ram += bundle.asan_reserve;

                        if memory.total_internal_ram > 0x800 {
                            errors.push(Error {
                                line: 0,
                                global: true,
                                message: "out of internal RAM".to_string(),
                                expanded_from: self.macro_context.clone(),
                                source: self.sources[0].clone(),
                            });
                        }
                    }
                } else if actual_name.starts_with("wr_") {
                    memory.total_working_ram += bundle.asan_reserve;

                    if memory.total_working_ram > 0x2000 {
                        errors.push(Error {
                            line: 0,
                            global: true,
                            message: "out of working RAM".to_string(),
                            expanded_from: self.macro_context.clone(),
                            source: self.sources[0].clone(),
                        });
                    }
                }

                memory.memory_ranges.push(range);
            }

            // Now that we have all the available memory ranges that are in the
            // scope of the address sanitizer, we can check on whether either of
            // them conflicts with another one. This has to be done in this
            // second iteration because `memory.memory_ranges` needed to be
            // filled with valid candidates first. This could potentially be
            // optimized later on, but so far I haven't seen any big performance
            // hints because of this (and, hey, going through the address
            // sanitizer is already a penalty hit compared to a regular run).
            for (i, range) in memory.memory_ranges.iter().enumerate() {
                for (j, existing) in memory.memory_ranges.iter().enumerate() {
                    if i == j {
                        continue;
                    }

                    if (range.range.start >= existing.range.start
                        && range.range.start < existing.range.end)
                        || (range.range.end > existing.range.start
                            && range.range.end < existing.range.end)
                    {
                        errors.push(Error {
                            line: 0,
                            global: true,
                            message: format!("The variable {range} conflicts with {existing}",),
                            expanded_from: self.macro_context.clone(),
                            source: self.sources[0].clone(),
                        });
                    }
                }
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    fn fill(&mut self) -> Result<Vec<Bundle>, Vec<Error>> {
        let mut errors = vec![];

        // Validate the mappings that have been evaluated before spitting it
        // out.
        if let Err(e) = crate::mapping::validate(&self.mappings) {
            return Err(vec![Error {
                line: 0,
                global: true,
                message: e,
                expanded_from: self.macro_context.clone(),
                source: self.sources[0].clone(),
            }]);
        }

        let mut res = vec![];

        for mapping in &mut self.mappings {
            for segment in mapping.segments.iter_mut() {
                if segment.is_empty() {
                    self.warnings.push(Error {
                        line: 0,
                        message: format!("segment '{}' is empty", segment.name),
                        source: self.sources[0].clone(),
                        expanded_from: self.macro_context.clone(),
                        global: true,
                    });
                }

                // The cheapest thing to do would be to call append(), but that
                // would remove all the elements from 'segment.bundles' and
                // that's not what we want in case the user wants to then
                // retrieve the bundles for each specific segment (e.g. the
                // '--split-segments' option on nasm). Hence, just go and copy
                // everything. Even if that's theoretically expensive it
                // shouldn't be that big of a deal considering the amount of
                // data we are moving here in the end.
                res.extend_from_slice(&segment.bundles);
            }

            let mut diff = mapping.size as isize - mapping.offset as isize;
            if diff < 0 {
                errors.push(Error{
                        line: 0,
                        message: format!(
                            "exceeding segment size for '{}'; expecting {} bytes and {} bytes have already been seen",
                            mapping.name, mapping.size, mapping.offset,
                        ),
                        source: self.sources[0].clone(),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                });
            }

            if let Some(fill) = mapping.fill {
                while diff > 0 {
                    res.push(Bundle::fill(fill));
                    diff -= 1;
                }
            }
        }

        if errors.is_empty() {
            Ok(res)
        } else {
            Err(errors)
        }
    }

    // Consume an .info/.warning/.error call.
    fn evaluate_echo(&mut self, node: &PNode, t: &EchoKind) -> Result<(), Vec<Error>> {
        let arg = node.args.as_ref().unwrap().first().unwrap();
        let val = &arg.value.value[1..arg.value.value.len() - 1];

        match t {
            EchoKind::Info => println!("info: {val}"),
            EchoKind::Warning => self.warnings.push(Error {
                global: false,
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                message: val.to_string(),
            }),
            EchoKind::Error => {
                return Err(Error {
                    global: false,
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    message: val.to_string(),
                }
                .into());
            }
        }

        Ok(())
    }

    // Consume a node which contains a macro call by pushing its bundles now.
    fn bundle_call(&mut self, node: &PNode) -> Result<(), Vec<Error>> {
        // Get the macro we are trying to reproduce.
        let mcr = *self.macros.get(&node.value.value).ok_or(Error {
            line: node.value.line,
            message: format!(
                "could not find a macro with the name '{}'",
                node.value.value
            ),
            expanded_from: self.macro_context.clone(),
            source: self.source_for(node),
            global: false,
        })?;

        // Detect missmatches between the number of arguments provided and the
        // ones defined by the macro.
        let given_args = node.args.as_ref().unwrap_or(&vec![]).len();
        let macro_args = mcr.args.as_ref().unwrap_or(&vec![]).len();
        if macro_args != given_args {
            return Err(Error {
                line: node.value.line,
                message: format!(
                    "wrong number of arguments for '{}': {} required but {} given",
                    node.value.value, macro_args, given_args,
                ),
                expanded_from: self.macro_context.clone(),
                source: self.source_for(node),
                global: false,
            }
            .into());
        }

        // All arguments are going to be added here as well so the inner block
        // can take it as a list of PendingDefine's. This way we can re-create
        // the values for the arguments used for this bundle call without having
        // to go over the full bundle call.
        let mut defines = vec![];

        // If there are arguments defined by the macro, set their values now.
        if given_args > 0 {
            let mut margs = mcr.args.as_ref().unwrap().iter();

            for arg in node.args.as_ref().unwrap().iter() {
                self.literal_mode = None;
                let obj = Object {
                    bundle: self.evaluate_node(arg)?,
                    node: Some(arg.clone()),
                    mapping: self.current_mapping,
                    segment: self.current_segment,
                    object_type: ObjectType::Argument,
                    asan_ignore: false,
                    asan_reserve: 1,
                    accessed: 0,
                };

                // Note that we overwrite the variable value from previous
                // calls, just in case a macro is applied multiple times and we
                // need to get the latest value.
                let id = &margs.next().unwrap().value;
                if let Err(message) = self.context.set_variable(id, &obj, true) {
                    return Err(Error {
                        line: node.value.line,
                        message,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    }
                    .into());
                }
                defines.push(PendingDefine {
                    id: id.clone(),
                    obj,
                });
            }
        }

        // And now replicate the nodes as contained inside of the macro
        // definition. In order to handle inner statements from macros such as
        // anonymous labels and stuff like that, we simply call again
        // `Assembler::bundle`. Note that the macro could have been malformed
        // (i.e. there was an error during parser time), or it could be empty
        // altogether. Just issue a warning on the latter case.
        let inner = &mcr.right.as_ref().unwrap().args.as_ref().unwrap();
        if inner.is_empty() {
            self.warnings.push(Error {
                line: node.value.line,
                message: format!("trying to apply empty macro '{}'", node.value.value),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        } else {
            self.macro_context.push(ExpandedFrom {
                line: node.value.line,
                source: self.source_for(node),
            });
            self.pending_defines.push(defines);
            self.bundle(inner)?;
            self.pending_defines.pop();
            let _ = self.macro_context.pop();
        }
        Ok(())
    }

    fn push_bundle(&mut self, mut bundle: Bundle, node: &PNode) -> Result<(), Error> {
        let current = &mut self.mappings[self.current_mapping];
        bundle.address = current.start as usize + current.offset;
        current.offset += bundle.size as usize;
        current.segments[self.current_segment].offset += bundle.size as usize;

        if !bundle.resolved {
            self.pending.push(PendingNode {
                mapping: self.current_mapping,
                segment: self.current_segment,
                context: self.context.name().to_string(),
                bundle_index: current.segments[self.current_segment].bundles.len(),
                node: node.to_owned(),
                labels_seen: self.context.labels_seen(),
                macro_context: self.macro_context.clone(),
                pending_defines: self.pending_defines.last().cloned(),
            });
        }
        current.segments[self.current_segment].bundles.push(bundle);

        Ok(())
    }

    fn evaluate_node(&mut self, node: &PNode) -> Result<Bundle, Error> {
        match &node.node_type {
            NodeType::Instruction => Ok(self.evaluate_instruction(node)?),
            NodeType::Literal => Ok(self.evaluate_literal(node)?),
            NodeType::Control(_) => Ok(self.evaluate_control_expression(node)?),
            NodeType::Operation(op) => Ok(self.evaluate_operation(node, op)?),
            NodeType::Value => match self.literal_mode {
                Some(LiteralMode::Hexadecimal) => Ok(self.evaluate_hexadecimal(node)?),
                Some(LiteralMode::Binary) => Ok(self.evaluate_binary(node)?),
                Some(LiteralMode::Plain) => Ok(self.evaluate_decimal(node)?),
                None => {
                    if node.value.value.chars().nth(0).unwrap_or(' ').is_numeric() {
                        // If the first digit is a number, let's actually try to
                        // parse it as a decimal value.
                        Ok(self.evaluate_decimal(node)?)
                    } else if node.value.is_anonymous_relative_reference() {
                        Ok(self.evaluate_anonymous_relative_reference(node)?)
                    } else if node.value.is_valid_identifier(true).is_err() {
                        // If this is not a valid identifier, just error out.
                        Err(Error {
                            message: "invalid identifier".to_string(),
                            line: node.value.line,
                            source: self.source_for(node),
                            expanded_from: self.macro_context.clone(),
                            global: false,
                        })
                    } else {
                        // This is actually a valid identifier! Try to fetch the
                        // variable.
                        match self.evaluate_variable(node) {
                            Ok(v) => {
                                self.literal_mode = Some(LiteralMode::Hexadecimal);
                                Ok(v)
                            }
                            Err(err) => Err(Error {
                                message: err.message,
                                line: node.value.line,
                                source: self.source_for(node),
                                expanded_from: self.macro_context.clone(),
                                global: false,
                            }),
                        }
                    }
                }
            },
            _ => Err(Error {
                message: format!("unexpected '{}' expression type", node.node_type),
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            }),
        }
    }

    // Evaluate the `node` by performing the operation described in
    // `operation_type` onto its arms.
    fn evaluate_operation(
        &mut self,
        node: &PNode,
        operation_type: &OperationType,
    ) -> Result<Bundle, Error> {
        // We first evaluate the node on the right, as unary operations will
        // only have a right node but not a left one. Moreover, this 'right'
        // object will be the one that will be updated so it can be returned.
        let mut right = self.evaluate_node(node.right.as_ref().unwrap())?;
        let rval = right.value();

        // Some operations need to evaluate a "left" node, and the literal mode
        // might have been changed by the previous evaluation of the "right"
        // node. Hence, reset it here to avoid problems.
        self.literal_mode = None;

        // Now perform the operation by using the right arm and the left one if
        // needed. If there was indeed a left value computed underneath, then
        // return it in the 'other' option. Otherwise 'res' contains the
        // arithmetic value, which might be bananas if the operation was done
        // between at least one unresolved value (see more on that below).
        let (res, other): (isize, Option<Bundle>) = match operation_type {
            OperationType::UnaryPositive => {
                right.negative = false;
                (rval.abs(), None)
            }
            OperationType::UnaryNegative => {
                right.negative = true;
                (rval.neg(), None)
            }
            OperationType::LogicalNot => ((rval == 0) as isize, None),
            OperationType::BitwiseNot => (!rval, None),
            OperationType::LoByte => {
                let r = (rval as u16).to_le_bytes();
                right.bytes[0] = r[0];
                right.bytes[1] = 0;
                right.size = 1;
                return Ok(right);
            }
            OperationType::HiByte => {
                let r = (rval as u16).to_le_bytes();
                right.bytes[0] = r[1];
                right.bytes[1] = 0;
                right.size = 1;
                return Ok(right);
            }
            OperationType::Add => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() + rval, Some(lval))
            }
            OperationType::Sub => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() - rval, Some(lval))
            }
            OperationType::Mul => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() * rval, Some(lval))
            }
            OperationType::Div => {
                if rval == 0 {
                    return Err(Error {
                        line: node.value.line,
                        global: false,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "attempting to divide by zero".to_string(),
                    });
                }
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() / rval, Some(lval))
            }
            OperationType::LogicalAnd => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (((lval.value() != 0) && (rval != 0)) as isize, Some(lval))
            }
            OperationType::And => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() & rval, Some(lval))
            }
            OperationType::LogicalOr => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (((lval.value() != 0) || (rval != 0)) as isize, Some(lval))
            }
            OperationType::Or => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() | rval, Some(lval))
            }
            OperationType::Xor => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() ^ rval, Some(lval))
            }
            OperationType::Lshift => {
                if rval as usize > 16 {
                    return Err(Error {
                        line: node.value.line,
                        global: false,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "shift operator too big".to_string(),
                    });
                }

                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() << rval, Some(lval))
            }
            OperationType::Rshift => {
                if rval as usize > 16 {
                    return Err(Error {
                        line: node.value.line,
                        global: false,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "shift operator too big".to_string(),
                    });
                }

                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                (lval.value() >> rval, Some(lval))
            }
            OperationType::Equal => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() == rval) as isize, Some(lval))
            }
            OperationType::NotEqual => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() != rval) as isize, Some(lval))
            }
            OperationType::Less => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() < rval) as isize, Some(lval))
            }
            OperationType::LessEqual => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() <= rval) as isize, Some(lval))
            }
            OperationType::Greater => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() > rval) as isize, Some(lval))
            }
            OperationType::GreaterEqual => {
                let lval = self.evaluate_node(node.left.as_ref().unwrap())?;
                ((lval.value() >= rval) as isize, Some(lval))
            }
        };

        // There are some corrections to be performed into the 'right' bundle if
        // there was another bundle involved in the operation.
        if let Some(o) = other {
            // Only mark this bundle as resolved if both sides of the operation
            // were resolved values.
            right.resolved = right.resolved && o.resolved;

            // If the size is different, then pick the larger one (e.g. '$2000 +
            // 1' is the same as '1 + $2000', which have a size of '2').
            if right.size < o.size {
                right.size = o.size;
            }
        }

        // Now, regardless of what we did whenever an 'other' object was
        // involved, we can do some final adjustments on the size of the Bundle
        // to be returned if the value is finally resolved. This is done by hand
        // as in some edge cases the size might be wrong just because one of the
        // sides or both of the operation might do funky things to their sizes
        // (e.g. '300 - 260' where both arms have values larger than a byte but
        // the end result should be one byte).
        if right.resolved {
            if res > 0x00FF {
                right.size = 2;
            } else {
                right.size = 1;
            }
        }

        // Prevent overflows, but only if the value is known for sure.
        if right.resolved && (res > i16::MAX.into() || res < i16::MIN.into()) {
            return Err(Error {
                line: node.value.line,
                global: false,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                message: "performing the operation would overflow a 16-bit integer".to_string(),
            });
        }

        // And, regardless of the size, we will set the bytes of the Bundle as
        // if it was a 16-bit value in little-endian format.
        let byte_result = (res as u16).to_le_bytes();
        right.bytes[0] = byte_result[0];
        right.bytes[1] = byte_result[1];

        Ok(right)
    }

    // Evaluate the given node as a relative reference.
    fn evaluate_anonymous_relative_reference(&mut self, node: &PNode) -> Result<Bundle, Error> {
        self.literal_mode = Some(LiteralMode::Plain);

        match &self.stage {
            Stage::Bundling => Ok(Bundle {
                bytes: [0, 0, 0],
                size: 2,
                address: 0,
                cycles: 0,
                affected_on_page: false,
                resolved: false,
                negative: false,
            }),
            Stage::Crunching => {
                match self.context.get_relative_label(
                    node.value.to_isize(),
                    self.labels_seen,
                    &self.mappings,
                ) {
                    Ok(object) => Ok(object.bundle),
                    Err(message) => Err(Error {
                        line: node.value.line,
                        message,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    }),
                }
            }
            Stage::Context => Err(Error {
                line: node.value.line,
                source: self.source_for(node),
                global: false,
                expanded_from: self.macro_context.clone(),
                message: "trying to evaluate a relative reference as a bare value".to_string(),
            }),
        }
    }

    fn evaluate_hexadecimal(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let mut chars = node.value.value.chars();
        let mut bytes = [0, 0, 0];
        let size: u8;

        match node.value.value.len() {
            1 => {
                bytes[0] = self.char_to_hex(chars.next(), node)?;
                size = 1;
            }
            2 => {
                bytes[0] = self.char_to_hex(chars.next(), node)? * 16;
                bytes[0] += self.char_to_hex(chars.next(), node)?;
                size = 1;
            }
            3 => {
                bytes[1] = self.char_to_hex(chars.next(), node)?;
                bytes[0] = self.char_to_hex(chars.next(), node)? * 16;
                bytes[0] += self.char_to_hex(chars.next(), node)?;
                size = 2;
            }
            4 => {
                bytes[1] = self.char_to_hex(chars.next(), node)? * 16;
                bytes[1] += self.char_to_hex(chars.next(), node)?;
                bytes[0] = self.char_to_hex(chars.next(), node)? * 16;
                bytes[0] += self.char_to_hex(chars.next(), node)?;
                size = 2;
            }
            _ => {
                if self.evaluate_variable(node).is_ok() {
                    return Err(Error {
                        message: format!(
                            "you cannot use variables like '{}' in hexadecimal literals",
                            node.value.value
                        ),
                        source: self.source_for(node),
                        line: node.value.line,
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    });
                }
                return Err(Error {
                    message: "expecting a number of 1 to 4 hexadecimal digits".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    global: false,
                });
            }
        }

        Ok(Bundle {
            bytes,
            size,
            address: 0,
            cycles: 0,
            affected_on_page: false,
            resolved: true,
            negative: false,
        })
    }

    fn evaluate_binary(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let string = node.value.value.as_str();
        let mut value = 0;

        // We are strict with the definition of binary values to avoid gotchas.
        // Force the literal to be exactly 8 digits wide. If that's not the
        // case, do not even try evaluating it.
        match string.len().cmp(&8) {
            Ordering::Less => {
                return Err(Error {
                    message: "missing binary digits to get a full byte".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    global: false,
                })
            }
            Ordering::Greater => {
                return Err(Error {
                    message: "too many binary digits for a single byte".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    global: false,
                })
            }
            _ => {}
        }

        for (shift, c) in string.chars().rev().enumerate() {
            if c == '1' {
                let val = 1 << shift;
                value += val;
            } else if c != '0' {
                if self.evaluate_variable(node).is_ok() {
                    return Err(Error {
                        message: format!(
                            "you cannot use variables like '{string}' in binary literals"
                        ),
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    });
                }
                return Err(Error {
                    message: format!("bad binary format for '{string}'"),
                    line: node.value.line,
                    global: false,
                    expanded_from: self.macro_context.clone(),
                    source: self.source_for(node),
                });
            }
        }

        Ok(Bundle {
            bytes: [value as u8, 0, 0],
            size: 1,
            address: 0,
            cycles: 0,
            affected_on_page: false,
            resolved: true,
            negative: false,
        })
    }

    fn evaluate_decimal(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let string = node.value.value.as_str();
        if string.is_empty() {
            return Err(Error {
                message: "empty decimal literal".to_string(),
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }

        let mut value = 0;
        let mut shift = 1;

        for c in string.chars().rev() {
            if shift > 10000 {
                return Err(Error {
                    message: "decimal value is too big".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    global: false,
                });
            }
            if c != '0' {
                match c.to_digit(10) {
                    Some(digit) => {
                        value += digit * shift;
                    }
                    None => {
                        if self.stage == Stage::Context {
                            return Err(Error {
                                message: format!(
                                    "variables must come from a constant expression, \
                                                  you cannot use other variables such as '{string}' \
                                                  in variable definitions"
                                ),
                                source: self.source_for(node),
                                line: node.value.line,
                                expanded_from: self.macro_context.clone(),
                                global: false,
                            });
                        }
                        match self.evaluate_variable(node) {
                            Ok(v) => return Ok(v),
                            Err(err) => {
                                return Err(Error {
                                    message: format!(
                                        "'{}' is not a decimal value and {} either",
                                        c, err.message
                                    ),
                                    source: self.source_for(node),
                                    line: node.value.line,
                                    expanded_from: self.macro_context.clone(),
                                    global: false,
                                });
                            }
                        }
                    }
                }
            }

            shift *= 10;
        }
        if value > u16::MAX.into() {
            return Err(Error {
                message: "decimal value is too big".to_string(),
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }

        let bytes = value.to_le_bytes();
        Ok(Bundle {
            bytes: [bytes[0], bytes[1], 0],
            size: if value > 255 { 2 } else { 1 },
            address: 0,
            cycles: 0,
            affected_on_page: false,
            resolved: true,
            negative: false,
        })
    }

    fn evaluate_literal(&mut self, node: &PNode) -> Result<Bundle, Error> {
        // The value of the literal is guaranteed to not be empty by the parser.
        // If that's not the case, then it's a bug.
        let val = node.value.value.as_str();
        assert!(!val.is_empty(), "the value for the literal was empty!");

        // Pick up the left node, which is the node to be further evaluated, and
        // determine the literal mode to be used.
        let left = node.left.as_ref().unwrap();

        let lm;
        if val.starts_with('$') {
            lm = Some(LiteralMode::Hexadecimal);
            if left.node_type == NodeType::Literal {
                return Err(Error {
                    message: "literal cannot embed another literal".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
        } else if val.starts_with('%') {
            lm = Some(LiteralMode::Binary);
            if left.node_type == NodeType::Literal {
                return Err(Error {
                    message: "literal cannot embed another literal".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
        } else {
            lm = Some(LiteralMode::Plain);
        }

        // And evaluate the left node.
        self.literal_mode = lm.clone();
        let expr = self.evaluate_node(left)?;
        self.literal_mode = lm;

        Ok(expr)
    }

    fn char_to_hex(&mut self, oc: Option<char>, source: &PNode) -> Result<u8, Error> {
        match oc {
            Some(c) => match c.to_digit(16) {
                Some(c) => Ok(c as u8),
                None => {
                    if (c.is_alphabetic() || c == '_') && self.evaluate_variable(source).is_ok() {
                        return Err(Error {
                            message: format!(
                                "you cannot use variables like '{}' in hexadecimal literals",
                                source.value.value
                            ),
                            line: source.value.line,
                            source: self.source_for(source),
                            global: false,
                            expanded_from: self.macro_context.clone(),
                        });
                    }
                    Err(Error {
                        message: "could not convert digit to hexadecimal".to_string(),
                        line: source.value.line,
                        source: self.source_for(source),
                        global: false,
                        expanded_from: self.macro_context.clone(),
                    })
                }
            },
            None => Err(Error {
                message: "digit out of bounds".to_string(),
                line: source.value.line,
                source: self.source_for(source),
                global: false,
                expanded_from: self.macro_context.clone(),
            }),
        }
    }

    fn evaluate_control_statement(&mut self, node: &'a PNode) -> Result<(), Vec<Error>> {
        // This might just be a statement that changes the context (e.g.
        // ".macro", ".proc", etc.). In this case change the context and leave
        // early.
        match self.context.change_context(node) {
            Ok(changed) => {
                if changed {
                    return Ok(());
                }
            }
            Err(message) => {
                return Err(Error {
                    message,
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                }
                .into());
            }
        }

        // Otherwise, check the function that could act as a statement that
        // produces bundles.
        match node.node_type {
            NodeType::Control(ControlType::Byte) => {
                Ok(self.push_evaluated_arguments(node, 1, true)?)
            }
            NodeType::Control(ControlType::Addr) | NodeType::Control(ControlType::Word) => {
                Ok(self.push_evaluated_arguments(node, 2, true)?)
            }
            NodeType::Control(ControlType::BigEndianWord) => {
                Ok(self.push_evaluated_arguments(node, 2, false)?)
            }
            NodeType::Control(ControlType::ReserveMemory) => Ok(self.reserve_memory(node)?),
            NodeType::Control(ControlType::Asciiz) => Ok(self.push_ascii_string(node)?),
            NodeType::Control(ControlType::Segment) => Ok(self.switch_to_segment(node)?),
            NodeType::Control(ControlType::IncBin) => {
                Ok(self.incbin(node.args.as_ref().unwrap().first().unwrap())?)
            }
            NodeType::Control(ControlType::If) => self.evaluate_if_block(node),
            NodeType::Control(ControlType::IfDef) | NodeType::Control(ControlType::IfNDef) => {
                self.evaluate_ifdef_block(node)
            }
            NodeType::Control(ControlType::EndIf) => Ok(()),
            NodeType::Control(ControlType::IncludeSource) => Ok(()),
            NodeType::Control(ControlType::Echo(_)) => Ok(()),
            _ => Err(Error {
                line: node.value.line,
                message: format!(
                    "cannot handle control statement '{}' in this context",
                    node.value.value
                ),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            }
            .into()),
        }
    }

    // Push as many bundles as bytes are in the given file path. If there is any
    // issue with reading the given file, or the parameter is given in a weird
    // format, it will error out.
    fn incbin(&mut self, node: &PNode) -> Result<(), Error> {
        let value = &node.value.value;

        // Validate the path literal.
        if value.len() < 3 || !value.starts_with('"') || !value.ends_with('"') {
            return Err(Error {
                line: node.value.line,
                message: format!(
                    "path has to be written inside of double quotes ('{value}' given instead)",
                ),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }

        // The '.incbin' control assumes that paths are relative to the
        // directory of the current file. Hence, in order to make subsequent
        // `File` operations work in this way, set the current directory now.
        match &self.sources.get(node.source) {
            Some(source) => {
                if let Err(e) = std::env::set_current_dir(&source.directory) {
                    return Err(Error {
                        line: node.value.line,
                        message: format!("could not move to the directory of '{value}': {e}"),
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    });
                }
            }
            None => panic!("mismatch on the node source"),
        }

        // Fetch the actual path.
        let path = &value[1..value.len() - 1].trim();
        let file = match File::open(path) {
            Ok(f) => f,
            Err(e) => {
                return Err(Error {
                    global: false,
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    message: format!("could not include binary data: {e}"),
                })
            }
        };

        // Ensure that the included binary data is within reason.
        match file.metadata() {
            Ok(metadata) => {
                // Note that we cannot assume that it's always going to be
                // included in some specific mapping type (e.g. CHR-ROM vs
                // CHR-RAM). Hence, let's force that nothing above 512KB can be
                // included at face value. If this really surpasses the actual
                // limit on where it's included, then it's going to show up at a
                // later check.
                if metadata.len() > 512 * 1024 {
                    return Err(Error {
                        global: false,
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: format!("file '{path}' is too big"),
                    });
                } else if metadata.len() == 0 {
                    return Err(Error {
                        global: false,
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: format!("trying to include an empty file ('{path}')"),
                    });
                }
            }
            Err(e) => {
                return Err(Error {
                    global: false,
                    line: node.value.line,
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    message: format!("could not include binary data: {e}"),
                })
            }
        }

        // And finally just push each byte from the given file as a fill bundle.
        let reader = BufReader::new(file);
        for byte in reader.bytes() {
            match byte {
                Ok(b) => self.push_bundle(Bundle::fill(b), node)?,
                Err(_) => break,
            }
        }
        Ok(())
    }

    // Evaluate the given node assuming it's a .repeat statement and push all
    // the requested bundles from it..
    fn evaluate_repeat_statement(&mut self, node: &'a PNode) -> Result<(), Vec<Error>> {
        // First fetch the number of times the code block must be repeated.
        let args = node.args.as_ref().unwrap();
        let first = &args.first().unwrap().value.value;
        let repeats = match first.parse::<usize>() {
            Ok(n) => {
                if n < 2 {
                    return Err(Error {
                        global: false,
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "pointless .repeat statement".to_string(),
                    }
                    .into());
                } else if n > 255 {
                    return Err(Error {
                        global: false,
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "the number of iterations has to fit in a single byte".to_string(),
                    }
                    .into());
                }
                n
            }
            Err(_) => {
                return Err(Error {
                    global: false,
                    line: node.value.line,
                    message: format!("first argument must be an integer, '{first}' found instead"),
                    expanded_from: self.macro_context.clone(),
                    source: self.source_for(node),
                }
                .into())
            }
        };

        // The code that is to be repeated.
        let code = &node.right.as_ref().unwrap().args.as_ref().unwrap();
        if code.is_empty() {
            self.warnings.push(Error {
                line: node.value.line,
                message: "empty .repeat statement".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
            return Ok(());
        }

        // Perform the action.
        for i in 0..repeats {
            // If an index was given, set it now as a .repeat variable with the
            // loop index.
            if args.len() == 2 {
                if let Err(e) = self.context.set_variable(
                    &args.last().unwrap().value,
                    &Object {
                        bundle: Bundle::fill(i as u8),
                        node: None,
                        mapping: self.current_mapping,
                        segment: self.current_segment,
                        asan_ignore: false,
                        object_type: ObjectType::Value,
                        asan_reserve: 1,
                        accessed: 0,
                    },
                    true,
                ) {
                    return Err(Error {
                        line: node.value.line,
                        message: e,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        global: false,
                    }
                    .into());
                }
            }

            // And push all the bundles from the inner code.
            self.bundle(code)?;
        }

        Ok(())
    }

    // Evaluate the given `node` by assuming that it's an .if/.elsif/.else
    // block.
    fn evaluate_if_block(&mut self, node: &'a PNode) -> Result<(), Vec<Error>> {
        // Evaluate the condition if possible. If that's not the case (i.e.
        // '.else'), then we just assume it to be true.
        let cond = match &node.args {
            Some(args) => {
                self.literal_mode = None;
                let cond_node = self.evaluate_node(args.first().unwrap())?;
                cond_node.value() == 1
            }
            None => true,
        };

        self.evaluate_if_cond(node, cond)
    }

    // Evaluate the given `node` by assuming that it's an .ifdef/.ifndef block.
    fn evaluate_ifdef_block(&mut self, node: &'a PNode) -> Result<(), Vec<Error>> {
        let defined = self.evaluate_defined(node)?.value();
        let cond = match &node.node_type {
            NodeType::Control(ControlType::IfDef) => defined == 1,
            NodeType::Control(ControlType::IfNDef) => defined == 0,
            _ => panic!("unexpected .ifdef block"),
        };

        self.evaluate_if_cond(node, cond)
    }

    // Evaluate the `node`'s body if `cond` is true, otherwise try to evaluate
    // the "else" branch for this .if-looking statement.
    fn evaluate_if_cond(&mut self, node: &'a PNode, cond: bool) -> Result<(), Vec<Error>> {
        // If condition was false, try to go into the .elsif/.else statement. If
        // that doesn't exist, then we are done.
        if !cond {
            match &node.left {
                Some(else_node) => return self.evaluate_if_block(else_node),
                None => return Ok(()),
            }
        }

        // And evaluate the inner block if the condition was true.
        let inner = &node.right.as_ref().unwrap().args.as_ref().unwrap();
        if inner.is_empty() {
            self.warnings.push(Error {
                line: node.value.line,
                message: "empty body".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        } else if self.stage == Stage::Context {
            self.eval_context(inner)?;
        } else {
            self.bundle(inner)?;
        }

        Ok(())
    }

    fn evaluate_control_expression(&mut self, node: &PNode) -> Result<Bundle, Error> {
        match node.node_type {
            NodeType::Control(ControlType::Hibyte) => self.evaluate_byte(node, true),
            NodeType::Control(ControlType::Lobyte) => self.evaluate_byte(node, false),
            NodeType::Control(ControlType::Defined) => self.evaluate_defined(node),
            NodeType::Control(ControlType::Version) => self.evaluate_version(node),
            NodeType::Control(ControlType::Max) => self.evaluate_max(node),
            NodeType::Control(ControlType::Min) => self.evaluate_min(node),
            _ => Err(Error {
                line: node.value.line,
                message: format!(
                    "cannot handle control statement '{}' as an expression in this context",
                    node.value.value
                ),
                expanded_from: self.macro_context.clone(),
                source: self.source_for(node),
                global: false,
            }),
        }
    }

    // Evaluate the args from 'node' and return the bundle from the node which
    // contains the maximum value.
    fn evaluate_max(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let Some(args) = &node.args else {
            return Ok(Bundle::default());
        };

        let mut max = Bundle::default();
        for arg in args {
            self.literal_mode = None;
            let bundle = self.evaluate_node(arg)?;

            if bundle.value() > max.value() {
                max = bundle.clone();
            }
        }
        Ok(max)
    }

    // Evaluate the args from 'node' and return the bundle from the node which
    // contains the minimum value.
    fn evaluate_min(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let Some(args) = &node.args else {
            return Ok(Bundle::default());
        };

        let mut min = Bundle::default();
        min.bytes[0] = 0xFF;
        min.bytes[1] = 0xFF;

        for arg in args {
            self.literal_mode = None;
            let bundle = self.evaluate_node(arg)?;

            if bundle.value() < min.value() {
                min = bundle.clone();
            }
        }
        Ok(min)
    }

    fn evaluate_byte(&mut self, node: &PNode, high: bool) -> Result<Bundle, Error> {
        // The parser actually guarantees that the ".hibyte" and ".lobyte"
        // functions have exactly one argument. Hence, if this is not the case,
        // it's fine to let "unwrap" panic: it's a sign that's something is
        // wrong elsewhere.
        let arg = node.args.as_ref().unwrap().first().unwrap();

        // Get the bundle from the argument
        let mut bundle = self.evaluate_node(arg)?;

        // The bundle we got is going to be shuffled if we wanted the high byte.
        // After that, just zero out the rest (not mandatory but let's do it out
        // of consistency) and set the size to just one byte.
        if high {
            bundle.bytes[0] = bundle.bytes[1];
        }
        bundle.bytes[1] = 0x00;
        bundle.bytes[2] = 0x00;
        bundle.size = 1;

        Ok(bundle)
    }

    // Returns a bundle which is just the version from this library.
    fn evaluate_version(&mut self, node: &PNode) -> Result<Bundle, Error> {
        let Ok(major) = env!("CARGO_PKG_VERSION_MAJOR").parse::<u8>() else {
            return Err(Error {
                line: node.value.line,
                message: "bad version number".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        };
        let Ok(minor) = env!("CARGO_PKG_VERSION_MINOR").parse::<u8>() else {
            return Err(Error {
                line: node.value.line,
                message: "bad version number".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        };

        Ok(Bundle {
            bytes: [minor, major, 0x00],
            size: 2,
            address: 0,
            cycles: 0,
            affected_on_page: false,
            resolved: true,
            negative: false,
        })
    }

    // Returns a bundle containing a boolean value on whether the symbol
    // referenced in the given `node` is actually defined on this context or
    // not.
    fn evaluate_defined(&mut self, node: &PNode) -> Result<Bundle, Error> {
        // Guaranteed by the parser to be only one argument.
        let name = node.args.as_ref().unwrap().first().unwrap();

        if self
            .context
            .get_variable(&name.value, &self.mappings)
            .is_ok()
        {
            Ok(Bundle::fill(0x01))
        } else {
            Ok(Bundle::fill(0x00))
        }
    }

    // Iterate through the arguments of 'node' and push the bundle that can be
    // evaluated from each argument. The caller expects this bundle to span
    // exactly 'nbytes' bytes, and if that is not met then zeroes should be
    // added. Last but not least, set 'little_endian' to true if you want the
    // bundle to be expressed in little endian format, or false if we should go
    // for big endian (i.e. an explicit '.be' is being used).
    fn push_evaluated_arguments(
        &mut self,
        node: &PNode,
        nbytes: u8,
        little_endian: bool,
    ) -> Result<(), Error> {
        match &node.args {
            Some(args) => {
                for arg in args {
                    // Evaluate the argument as a node.
                    self.literal_mode = None;
                    let mut bundle = self.evaluate_node(arg)?;

                    // If there is a missmatch between the expected number of
                    // bytes and what we got, we might be able to resolve it if
                    // we expected two bytes and only one was received: extend
                    // it by leading zeroes. Otherwise we have to error out:
                    // it's up to the programmer to either call `.hibyte` or
                    // something similar if that's whay they intended.
                    if bundle.size != nbytes {
                        match nbytes {
                            1 => {
                                return Err(Error {
                                    line: arg.value.line,
                                    message: "expecting an argument that fits into a byte"
                                        .to_string(),
                                    source: self.source_for(node),
                                    expanded_from: self.macro_context.clone(),
                                    global: false,
                                })
                            }
                            2 => {
                                bundle.size = 2;
                                if little_endian {
                                    bundle.bytes[1] = 0x00;
                                }
                                bundle.bytes[2] = 0x00;
                            }
                            _ => panic!("bad argument when evaluating arguments"),
                        }
                    }

                    // If this was supposed to be a big endian word, swap the
                    // bytes we've got before pushing the bundle.
                    if !little_endian {
                        bundle.bytes.swap(1, 0);
                    }

                    self.push_bundle(bundle, arg)?;
                }
            }
            None => {
                return Err(Error {
                    line: node.value.line,
                    message: format!(
                        "expecting at least one argument for '{}'",
                        node.value.value.as_str(),
                    ),
                    source: self.source_for(node),
                    expanded_from: self.macro_context.clone(),
                    global: false,
                })
            }
        }

        Ok(())
    }

    // Consume the given `node` by pushing as many fill bundles as it can be
    // parsed assuming this is a `.res` control statement.
    fn reserve_memory(&mut self, node: &PNode) -> Result<(), Error> {
        let args = node.args.as_ref().unwrap();

        // Fetch the number or bytes to consume.
        let n = self.evaluate_node(&args[0])?;
        if n.size > 2 {
            return Err(Error {
                global: false,
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                message: "you are trying to reserve too much memory".to_string(),
            });
        }
        let iterations = u16::from_le_bytes([n.bytes[0], n.bytes[1]]);
        if iterations == 0 {
            return Err(Error {
                global: false,
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                message: "empty .res statement".to_string(),
            });
        } else if iterations == 1 {
            self.warnings.push(Error {
                global: false,
                line: node.value.line,
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                message: "pointless .res statement, prefer using .byte".to_string(),
            });
        }

        // Check whether the fill value was provided. If that's not the case,
        // then fetch it from the current mapping.
        let fill = match args.get(1) {
            Some(fill) => {
                let bundle = self.evaluate_node(fill)?;
                if bundle.size > 1 {
                    return Err(Error {
                        global: false,
                        line: node.value.line,
                        source: self.source_for(node),
                        expanded_from: self.macro_context.clone(),
                        message: "fill value must fit into a single byte".to_string(),
                    });
                }
                bundle.bytes[0]
            }
            None => {
                let mapping = &self.mappings[self.current_mapping];
                mapping.fill.unwrap_or(0x00)
            }
        };

        // And push as many fill bundles as requested with the evaluated fill
        // value.
        for _i in 0..iterations {
            self.push_bundle(Bundle::fill(fill), node)?;
        }

        Ok(())
    }

    // Fetch the first argument from `node`, validate that it's a quoted string,
    // and return its inner value. Note that before calling this function there
    // must be the guarantee that there's at least one argument inside of the
    // node.
    fn fetch_quoted_first_argument(&mut self, node: &'a PNode) -> Result<&'a str, Error> {
        let arg = &node.args.as_ref().unwrap().first().unwrap();
        let val = &arg.value.value;
        if val.len() < 3 {
            return Err(Error {
                line: node.value.line,
                message: "unpermitted empty argument".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }
        if !val.starts_with('"') || !val.ends_with('"') {
            return Err(Error {
                line: node.value.line,
                message: format!(
                    "declaration has to be written inside of double quotes ('{val}' given instead)",
                ),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }

        Ok(&val[1..val.len() - 1])
    }

    // Push fill bundles which correspond to the string referenced in `node`. An
    // 0x00 bundle will also be pushed at the end.
    fn push_ascii_string(&mut self, node: &'a PNode) -> Result<(), Error> {
        let string = self.fetch_quoted_first_argument(node)?;

        for ch in string.chars() {
            // NOTE: the parser guarantees that a string literal only contains
            // ASCII characters.
            self.push_bundle(Bundle::fill(ch as u8), node)?;
        }
        self.push_bundle(Bundle::fill(0x00), node)?;

        Ok(())
    }

    // Change the current segment to the one referenced in `node`.
    fn switch_to_segment(&mut self, node: &'a PNode) -> Result<(), Error> {
        let name = self.fetch_quoted_first_argument(node)?;
        if name
            .chars()
            .any(|ch| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'))
        {
            return Err(Error {
                line: node.value.line,
                message: "segment name contains bad characters".to_string(),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }

        // Find the segment being referenced and update the
        // `self.current_segment` accordingly.
        let mut found = false;
        for (mapping_idx, mapping) in self.mappings.iter().enumerate() {
            for (segment_idx, segment) in mapping.segments.iter().enumerate() {
                if segment.name == name {
                    self.current_mapping = mapping_idx;
                    self.current_segment = segment_idx;
                    found = true;
                    break;
                }
            }
        }
        if !found {
            return Err(Error {
                line: node.value.line,
                message: format!("unknown segment '{name}'"),
                source: self.source_for(node),
                expanded_from: self.macro_context.clone(),
                global: false,
            });
        }
        Ok(())
    }

    fn evaluate_variable(&mut self, node: &PNode) -> Result<Bundle, Error> {
        match self.context.get_variable(&node.value, &self.mappings) {
            Ok(mut value) => {
                // If the given variable has a zero value AND we are resolving
                // pending nodes AND the variable has a node object in it, then
                // chances are that this is a macro call with an argument to be
                // resolved. Hence, the value is not this empty variable but the
                // node to be resolved which is contained on this "variable".
                if self.stage == Stage::Crunching && value.bundle.is_zero() && value.node.is_some()
                {
                    // Evaluate the node, which has the proper (resolved at this
                    // stage) value.
                    let bundle = self.evaluate_node(&value.node.clone().unwrap())?;

                    // Overwrite the variable with this new value. This way the
                    // next time this is found we don't have to evaluate it
                    // again.  If the variable could not be set, then it's not
                    // that big of a deal at this stage.
                    value.bundle = bundle.clone();
                    let _ = self.context.set_variable(&node.value, &value, true);

                    // And return the computed bundle.
                    Ok(bundle)
                } else {
                    // In most cases returning the fetched bundle would just be
                    // fine, but if it's not resolved then the caller might not
                    // be able to make assumptions on the end size, which is
                    // important because we might be at a state of
                    // 'Stage::Bundling' and we _must_ know the sizes of each
                    // instruction as addresses are being computed along the
                    // way. Hence, we might not know the value right now if it's
                    // not resolved yet, but if it's an address (which is known
                    // when defining the variable at a state of
                    // 'Stage::Context'), then we definitely know the size and
                    // the caller can make this assumption (which might
                    // determine a ZeroPageX or an IndirectX instruction, with a
                    // byte in size of difference).
                    let mut bundle = value.bundle;
                    if matches!(value.object_type, ObjectType::Address) {
                        bundle.size = 2;
                    }
                    Ok(bundle)
                }
            }
            Err(e) => Err(Error {
                message: e,
                line: node.value.line,
                source: self.source_for(node),
                global: false,
                expanded_from: self.macro_context.clone(),
            }),
        }
    }

    fn evaluate_instruction(&mut self, node: &PNode) -> Result<Bundle, Error> {
        self.literal_mode = None;

        let (mode, mut bundle) = match &node.left {
            Some(_) => self.get_addressing_mode_and_bytes(node)?,
            None => (AddressingMode::Implied, Bundle::new(true)),
        };

        // If the instruction is accessing memory and the bundle is resolved, we
        // can start gathering info on the memory being used.
        if bundle.resolved && !matches!(mode, AddressingMode::Implied | AddressingMode::Immediate) {
            let value = bundle.value() as usize;
            if (0x6000..0x8000).contains(&value) {
                self.accessing_working_ram = true;
            }
        }

        let mnemonic = node.value.value.to_lowercase();
        match INSTRUCTIONS.get(&mnemonic) {
            Some(entries) => match entries.get(&mode) {
                Some(values) => {
                    bundle.cycles = values.cycles;
                    bundle.size = values.size;
                    bundle.affected_on_page = values.affected_on_page;
                    bundle.bytes[2] = bundle.bytes[1];
                    bundle.bytes[1] = bundle.bytes[0];
                    bundle.bytes[0] = values.opcode.to_le_bytes()[0];
                }
                None => {
                    return Err(Error {
                        message: format!(
                            "cannot use {mode} addressing mode for the instruction '{mnemonic}'"
                        ),
                        source: self.source_for(node),
                        line: node.value.line,
                        global: false,
                        expanded_from: self.macro_context.clone(),
                    });
                }
            },
            None => {
                return Err(Error {
                    message: format!("unknown instruction {mnemonic}"),
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
        }
        Ok(bundle)
    }

    fn get_addressing_mode_and_bytes(
        &mut self,
        node: &PNode,
    ) -> Result<(AddressingMode, Bundle), Error> {
        let left = &node.left;

        if left.as_ref().unwrap().node_type == NodeType::Indirection {
            self.get_from_indirect(node)
        } else if node.right.is_some() {
            self.get_from_indexed(node)
        } else {
            self.get_from_left(node, left.as_ref().unwrap())
        }
    }

    fn get_from_indirect(&mut self, node: &PNode) -> Result<(AddressingMode, Bundle), Error> {
        let left = node.left.as_ref().unwrap();

        match node.right.as_ref() {
            Some(right) => {
                if right.value.value.trim().to_lowercase() == "y" {
                    if left.right.is_some() {
                        return Err(Error {
                            message:
                                "it has to be either X addressing or Y addressing, not all at once"
                                    .to_string(),
                            line: node.value.line,
                            source: self.source_for(node),
                            global: false,
                            expanded_from: self.macro_context.clone(),
                        });
                    }

                    let evaluated_node = left.left.as_ref().unwrap();
                    let val = self.evaluate_node(evaluated_node)?;
                    if val.size != 1 {
                        return Err(Error {
                            message: "address can only be one byte long on indirect Y addressing"
                                .to_string(),
                            line: node.value.line,
                            source: self.source_for(node),
                            global: false,
                            expanded_from: self.macro_context.clone(),
                        });
                    }
                    self.asan_check_arm(evaluated_node, &val)?;
                    return Ok((AddressingMode::IndirectY, val));
                }
                Err(Error {
                    message: "only the Y index is allowed on indirect Y addressing".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                })
            }
            None => match left.right.as_ref() {
                Some(right) => {
                    if right.value.value.trim().to_lowercase() == "x" {
                        let evaluated_node = left.left.as_ref().unwrap();
                        let val = self.evaluate_node(evaluated_node)?;
                        if val.size != 1 {
                            return Err(Error {
                                message:
                                    "address can only be one byte long on indirect X addressing"
                                        .to_string(),
                                line: node.value.line,
                                source: self.source_for(node),
                                global: false,
                                expanded_from: self.macro_context.clone(),
                            });
                        }
                        self.asan_check_arm(evaluated_node, &val)?;
                        return Ok((AddressingMode::IndirectX, val));
                    }
                    Err(Error {
                        message: "only the X index is allowed on indirect X addressing".to_string(),
                        line: node.value.line,
                        source: self.source_for(node),
                        global: false,
                        expanded_from: self.macro_context.clone(),
                    })
                }
                None => {
                    let evaluated_node = left.left.as_ref().unwrap();
                    let mut val = self.evaluate_node(evaluated_node)?;

                    // If this is a valid pure indirect addressing, then we know
                    // for sure we are in a 'jmp' instruction. In this case, if
                    // a zero-page address was being used, expand it to an
                    // absolute address. We do this unconditionally just to be
                    // sure, just as like we set the high byte of the address to
                    // $00 just to sanitize things.
                    val.size = 3;
                    val.bytes[2] = 0x00;

                    self.asan_check_arm(evaluated_node, &val)?;
                    Ok((AddressingMode::Indirect, val))
                }
            },
        }
    }

    fn get_from_indexed(&mut self, node: &PNode) -> Result<(AddressingMode, Bundle), Error> {
        // Evaluate the left arm of the instruction.
        let left = node.left.as_ref().unwrap();
        let mut val = self.evaluate_node(left)?;

        // Ensure that the literal mode for the left arm ensures an address
        // instead of some bogus number.
        if let Some(lm) = &self.literal_mode {
            if *lm != LiteralMode::Hexadecimal {
                return Err(Error {
                    message: "indexed addressing only works with addresses".to_string(),
                    line: node.value.line,
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
        }

        // Check the right arm to know the index being used.
        let right = node.right.as_ref().unwrap();
        match right.value.value.to_lowercase().trim() {
            "x" => {
                self.asan_check_arm(left, &val)?;

                // If the size == 2 but we can fit it on a single byte (i.e.
                // because the second byte is just 0x00), then just "compress"
                // this instruction. Note that this is only valid if the value
                // is fully well-known (i.e. it's not yet to be resolved). When
                // the value is not yet resolved, it usually revolves around an
                // address being referenced, which is never on the zeropage
                // section, so it wouldn't fit on a single byte anyways.
                if val.size == 1 || (val.resolved && val.bytes[1] == 0x00) {
                    // If despite all of the above the parsed instruction +
                    // zeropage indexing is actually not valid but the absolute
                    // indexing is, then convert this instruction to absolute
                    // indexing.
                    let mnemonic = node.value.value.to_lowercase();
                    if let Some(entries) = INSTRUCTIONS.get(&mnemonic) {
                        if entries.get(&AddressingMode::ZeropageIndexedX).is_none()
                            && entries.get(&AddressingMode::IndexedX).is_some()
                        {
                            val.size = 2;
                            return Ok((AddressingMode::IndexedX, val));
                        }
                    }

                    // Re-inforce the optimization when val.size == 2 by forcing
                    // the size to 1.
                    val.size = 1;
                    Ok((AddressingMode::ZeropageIndexedX, val))
                } else {
                    Ok((AddressingMode::IndexedX, val))
                }
            }
            "y" => {
                self.asan_check_arm(left, &val)?;

                // Same optimization as with the "x" case.
                if val.size == 1 || (val.resolved && val.bytes[1] == 0x00) {
                    // Similar to the case on "x" indexing.
                    let mnemonic = node.value.value.to_lowercase();
                    if let Some(entries) = INSTRUCTIONS.get(&mnemonic) {
                        if entries.get(&AddressingMode::ZeropageIndexedY).is_none()
                            && entries.get(&AddressingMode::IndexedY).is_some()
                        {
                            val.size = 2;
                            return Ok((AddressingMode::IndexedY, val));
                        }
                    }

                    val.size = 1;
                    Ok((AddressingMode::ZeropageIndexedY, val))
                } else {
                    Ok((AddressingMode::IndexedY, val))
                }
            }
            _ => Err(Error {
                message: "can only use X and Y as indices".to_string(),
                line: node.value.line,
                source: self.source_for(node),
                global: false,
                expanded_from: self.macro_context.clone(),
            }),
        }
    }

    fn get_from_left(
        &mut self,
        base: &PNode,
        left_arm: &PNode,
    ) -> Result<(AddressingMode, Bundle), Error> {
        if left_arm.value.value.to_lowercase().trim() == "a" {
            return Ok((AddressingMode::Implied, Bundle::new(true)));
        }

        let mut val = self.evaluate_node(left_arm)?;
        match self.literal_mode {
            Some(LiteralMode::Hexadecimal) => {
                // As for checking the most significant byte, it's the same
                // optimization as with absolute to zeropage indexed addressing.
                if base.is_branch() {
                    val.size = 1;
                    Ok((AddressingMode::RelativeOrZeropage, val))
                } else if val.size == 1
                    || (val.bytes[1] == 0x00 && !matches!(base.value.value.as_str(), "jmp" | "jsr"))
                {
                    self.asan_check_arm(left_arm, &val)?;
                    val.size = 1;
                    Ok((AddressingMode::RelativeOrZeropage, val))
                } else {
                    if !matches!(base.value.value.as_str(), "jmp" | "jsr") {
                        self.asan_check_arm(left_arm, &val)?;
                    }
                    Ok((AddressingMode::Absolute, val))
                }
            }
            Some(LiteralMode::Plain) => {
                if base.is_branch() {
                    Ok((AddressingMode::RelativeOrZeropage, val))
                } else if val.size > 1 {
                    match base.value.value.as_str() {
                        "jmp" | "jsr" => Ok((AddressingMode::Absolute, val)),
                        _ => Err(Error {
                            message: "immediate is too big".to_string(),
                            line: left_arm.value.line,
                            source: self.source_for(base),
                            global: false,
                            expanded_from: self.macro_context.clone(),
                        }),
                    }
                } else {
                    Ok((AddressingMode::Immediate, val))
                }
            }
            _ => Err(Error {
                message: "left arm of instruction is neither an address nor an immediate"
                    .to_string(),
                line: left_arm.value.line,
                source: self.source_for(base),
                global: false,
                expanded_from: self.macro_context.clone(),
            }),
        }
    }

    // Check the `node` considering is an instructions' "left" arm. More than
    // that, it assumes that it's a memory access. Hence, only call this
    // function if the addressing mode allows for it. Moreover, a `value` is
    // also supplied to perform further checks on the given memory access.
    //
    // Note that it will only run if the address sanitizer is enabled, and it
    // will check that any access only occurs as a variable or a simple pointer
    // arithmetic. Also note that this function assumes that it's not being
    // called by `jmp` or `jsr` kind of instructions on the absolute addressing
    // mode, as they operate on another level.
    fn asan_check_arm(&mut self, node: &PNode, value: &Bundle) -> Result<(), Error> {
        if !self.asan_enabled || self.asan_next_ignore {
            return Ok(());
        }

        match node.node_type {
            NodeType::Value => {
                let name = &node.value.value;
                if !is_asan_friendly_name(name) {
                    // If it's referencing an actual ObjectType::Address, then
                    // let it be (e.g. 'lda palettes, x'; where 'palettes' is a
                    // legitimate name even if not 'is_asan_friendly_name').
                    if let Ok(var) = self.context.get_variable(&node.value, &self.mappings) {
                        if matches!(var.object_type, ObjectType::Address | ObjectType::Argument) {
                            return Ok(());
                        }
                    }

                    // Everything has been exhausted, this is actually not a
                    // good name access.
                    self.warnings.push(Error {
                        line: node.value.line,
                        message: format!(
                            "accessing a memory region without a proper name ('{name}')"
                        ),
                        source: self.source_for(node),
                        global: false,
                        expanded_from: self.macro_context.clone(),
                    });
                }
                Ok(())
            }
            NodeType::Operation(OperationType::Add) | NodeType::Operation(OperationType::Sub) => {
                // Get the name of the variable involved.
                let left_name = &node.left.as_ref().unwrap().value;
                let right_name = &node.left.as_ref().unwrap().value;
                let name = if is_asan_friendly_name(&left_name.value) {
                    node.left.as_ref().unwrap()
                } else if is_asan_friendly_name(&right_name.value) {
                    node.right.as_ref().unwrap()
                } else {
                    // If everything failed but because it was an address
                    // (e.g. 'lda palettes + 1, x'), then return early.
                    if let Ok(var) = self.context.get_variable(left_name, &self.mappings) {
                        if matches!(var.object_type, ObjectType::Address | ObjectType::Argument) {
                            return Ok(());
                        }
                    }
                    if let Ok(var) = self.context.get_variable(right_name, &self.mappings) {
                        if matches!(var.object_type, ObjectType::Address | ObjectType::Argument) {
                            return Ok(());
                        }
                    }

                    self.warnings.push(Error {
                        line: node.value.line,
                        message: "accessing a memory region without a proper name".to_string(),
                        source: self.source_for(node),
                        global: false,
                        expanded_from: self.macro_context.clone(),
                    });
                    return Ok(());
                };

                // Fetch the variable. If it doesn't exist or its value hasn't
                // been resolved yet, we will ignore this check.
                match self.context.get_variable(&name.value, &self.mappings) {
                    Ok(object) => {
                        // If it has a resolved value, then check that the
                        // pointer arithmetics are within reserved bounds.
                        if object.bundle.resolved {
                            let original_value = object.bundle.value() as usize;
                            let given_value = value.value() as usize;
                            let end = original_value + object.asan_reserve;

                            if given_value < original_value || given_value >= end {
                                let range = MemoryRange {
                                    name: name.value.value.clone(),
                                    range: (original_value..end),
                                };
                                return Err(Error {
                                    line: node.value.line,
                                    message: format!("out of bounds memory access for {range}"),
                                    source: self.source_for(node),
                                    global: false,
                                    expanded_from: self.macro_context.clone(),
                                });
                            }
                        }
                        Ok(())
                    }
                    Err(_) => Ok(()),
                }
            }
            _ => {
                self.warnings.push(Error {
                    line: node.value.line,
                    message: "accessing a memory region without using a variable".to_string(),
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });

                Ok(())
            }
        }
    }

    // Check that the variable name follows the coding convention as expected
    // from the address sanitizer.
    fn asan_check_variable_name(&mut self, node: &PNode, bundle: &Bundle) -> Result<(), Error> {
        // If this is not an asan-friendly name, skip it, as it might be a
        // constant or something else that might be catched later.
        if !is_asan_friendly_name(&node.value.value) {
            return Ok(());
        }

        // If the bundle was explicitely marked as negative, then cry out.
        if bundle.negative {
            return Err(Error {
                line: node.value.line,
                message: "you cannot use a negative value for a memory variable".to_string(),
                source: self.source_for(node),
                global: false,
                expanded_from: self.macro_context.clone(),
            });
        }

        // Let's create this object now to makes things easier for possible
        // error messages.
        let actual_name = node.value.value.split("::").last().unwrap_or("");

        if actual_name.starts_with("zp_") {
            if bundle.size > 1 {
                let value = bundle.value() as usize;
                let range = MemoryRange {
                    name: node.value.value.to_owned(),
                    range: (value..(value + self.asan_next_reserve)),
                };
                self.warnings.push(Error {
                    line: node.value.line,
                    message: format!("you are assigning the zero-page variable {range} to something outside of zero page"),
                    source: self.source_for(node),
                    global: false,
                        expanded_from: self.macro_context.clone(),
                });
            }
        } else if bundle.size != 2 {
            let value = bundle.value() as usize;
            let range = MemoryRange {
                name: node.value.value.to_owned(),
                range: (value..(value + self.asan_next_reserve)),
            };
            self.warnings.push(Error {
                line: node.value.line,
                message: format!(
                    "you are assigning the variable {range} to something in zero page"
                ),
                source: self.source_for(node),
                global: false,
                expanded_from: self.macro_context.clone(),
            });
        } else if actual_name.starts_with("wr_") {
            let value = bundle.value() as usize;
            let range = MemoryRange {
                name: node.value.value.to_owned(),
                range: (value..(value + self.asan_next_reserve)),
            };
            if !(0x6000..0x8000).contains(&value) {
                self.warnings.push(Error {
                    line: node.value.line,
                    message: format!(
                        "you are assigning the variable {range} to something outside of Working RAM"
                    ),
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
        }

        Ok(())
    }

    fn to_relative_address(&self, node: &PNode, bundle: &mut Bundle) -> Result<(), Error> {
        if !bundle.resolved {
            return Ok(());
        }

        let next = (bundle.address + 2) as u16;
        let target = u16::from_le_bytes([bundle.bytes[1], bundle.bytes[2]]);

        let byte = if target < next {
            let diff = target as i16 - next as i16;
            if diff < -128 {
                return Err(Error {
                    line: node.value.line,
                    message: "you cannot branch to this location: it's too far away".to_string(),
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
            diff.to_le_bytes()[0]
        } else {
            let diff = target - next;
            if diff > 127 {
                return Err(Error {
                    line: node.value.line,
                    message: "you cannot branch to this location: it's too far away".to_string(),
                    source: self.source_for(node),
                    global: false,
                    expanded_from: self.macro_context.clone(),
                });
            }
            diff.to_le_bytes()[0]
        };

        bundle.bytes[1] = byte;
        bundle.bytes[2] = 0;
        bundle.size = 2;

        Ok(())
    }

    // Builds a SourceInfo object based on the given node.
    fn source_for(&self, node: &PNode) -> SourceInfo {
        self.sources
            .get(node.source)
            .unwrap_or(&SourceInfo {
                directory: self.sources[0].directory.clone(),
                name: self.sources[0].name.clone(),
            })
            .clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mapping::{get_mapping_configuration, SectionType, Segment};

    fn one_two() -> Vec<Mapping> {
        vec![
            Mapping {
                name: String::from("HEADER"),
                start: 0x0000,
                size: 0x0010,
                offset: 0,
                fill: Some(0x00),
                section_type: SectionType::Header,
                segments: vec![Segment::from("HEADER")],
            },
            Mapping {
                name: String::from("ROM0"),
                start: 0x8000,
                size: 0x8000,
                offset: 0,
                fill: None,
                section_type: SectionType::PrgRom,
                segments: vec![Segment::from("ONE"), Segment::from("TWO")],
            },
        ]
    }

    fn empty() -> Vec<Mapping> {
        get_mapping_configuration("empty").unwrap()
    }

    fn minimal_header() -> &'static str {
        // NES\0
        // 1 * 8KB of PRG ROM
        // No CHR ROM
        r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00
.segment "CODE"
"#
    }

    // Just run `assemble_with_mapping` by assuming `minimal_header()` and an
    // empty mapper.
    fn just_assemble(line: &str) -> AssemblerResult {
        // The line cannot be given as is, but we have to prepend the header or
        // the assembler will freak out.
        let real_line = minimal_header().to_string() + line;

        assemble_with_mapping(
            real_line.as_bytes(),
            empty(),
            &[],
            &SourceInfo::default(),
            false,
        )
    }

    // Like `just_assemble` but it only returns bundles passed the header.
    fn just_bundles(line: &str) -> Vec<Bundle> {
        let res = just_assemble(line);

        assert_eq!(res.errors.len(), 0);
        res.bundles[0x10..].to_vec()
    }

    // Given an instruction in `line`, assert that the resulting bundle matches
    // the given `hex` values.
    fn assert_instruction(line: &str, hex: &[u8]) {
        let res = just_assemble(line);

        assert!(res.warnings.is_empty());
        assert!(res.errors.is_empty());

        assert_eq!(res.bundles.len(), 0x11);

        let bundles = &res.bundles[0x10..];
        for i in 0..bundles[0].size {
            assert_eq!(hex[i as usize], bundles[0].bytes[i as usize]);
        }
    }

    // Given a code in `line`, assemble it and assert that an error with the
    // given `message` is reported. The line number `line_num` should also
    // appear if `global` is set to false. Note that the line number doesn't
    // have to account for the inserted header.
    fn assert_error(line: &str, line_num: usize, global: bool, message: &str) {
        let res = just_assemble(line);
        let msg = if global {
            message.to_string()
        } else {
            // +3 to account for the header.
            format!("{} (line {})", message, line_num + 3)
        };

        assert!(res.bundles.is_empty());
        assert!(!res.errors.is_empty());
        assert_eq!(res.errors[0].to_string().as_str(), msg);
    }

    // Empty

    #[test]
    fn empty_line() {
        for line in vec!["", "  ", ";; Comment", "  ;; Comment"].into_iter() {
            let res = just_assemble(line);

            assert!(res.errors.is_empty());
            assert!(res.bundles[0x10..].is_empty());

            // Warning for empty CODE segment.
            assert_eq!(res.warnings.len(), 1);
            assert_eq!(res.warnings[0].to_string(), "segment 'CODE' is empty");
        }
    }

    // Literal modes

    #[test]
    fn parse_binary() {
        assert_error(
            "adc #%0001",
            1,
            false,
            "missing binary digits to get a full byte",
        );
        assert_error(
            "adc #%0001000",
            1,
            false,
            "missing binary digits to get a full byte",
        );
        assert_error(
            "adc #%000100001",
            1,
            false,
            "too many binary digits for a single byte",
        );
        assert_error(
            r#"
            Variable = 42
            adc %Variable
            "#,
            3,
            false,
            "you cannot use variables like 'Variable' in binary literals",
        );
        assert_instruction("adc #%10100010", &[0x69, 0xA2]);
    }

    #[test]
    fn parse_hexadecimal() {
        assert_error(
            "adc #$12345",
            1,
            false,
            "expecting a number of 1 to 4 hexadecimal digits",
        );
        assert_error(
            "adc $AW",
            1,
            false,
            "could not convert digit to hexadecimal",
        );
        assert_error(
            r#"
    Variable = 42
    adc $Variable
    "#,
            3,
            false,
            "you cannot use variables like 'Variable' in hexadecimal literals",
        );
        assert_error(
            r#"
    Four = 4
    adc $Four
    "#,
            3,
            false,
            "you cannot use variables like 'Four' in hexadecimal literals",
        );
        assert_instruction("adc $AA", &[0x65, 0xAA]);
        assert_instruction("adc $10", &[0x65, 0x10]);
        assert_instruction("adc $10AB", &[0x6D, 0xAB, 0x10]);
    }

    #[test]
    fn parse_decimal() {
        assert_error("adc #222256", 1, false, "decimal value is too big");
        assert_error(
                "adc #2A",
                1, false,
                "'A' is not a decimal value and could not find variable '2A' in the global scope either",
            );
        assert_instruction("adc #1", &[0x69, 0x01]);
        assert_instruction("adc #.hibyte(61953)", &[0x69, 0xF2]);
    }

    // Variables

    #[test]
    fn scoped_variable() {
        let res = just_bundles(
            r#"
    .scope One   ; This is a comment
      adc #Variable

      Variable = $20
    .endscope

    .scope Another
      Variable = $40
    .endscope

    Variable = $30
    adc #Variable

    adc #One::Variable
    adc #Another::Variable
    "#,
        );

        assert_eq!(res.len(), 4);
        let instrs: Vec<[u8; 2]> = vec![[0x69, 0x20], [0x69, 0x30], [0x69, 0x20], [0x69, 0x40]];

        for i in 0..4 {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn bare_variables() {
        let res = just_bundles(
            r#"
    Variable = 4
    adc Variable
    adc Variable + 1
    adc Variable+1
    "#,
        );

        assert_eq!(res.len(), 3);

        assert_eq!(res[0].size, 2);
        assert_eq!(res[0].bytes[0], 0x65);
        assert_eq!(res[0].bytes[1], 0x04);

        assert_eq!(res[1].size, 2);
        assert_eq!(res[1].bytes[0], 0x65);
        assert_eq!(res[1].bytes[1], 0x05);

        assert_eq!(res[2].size, 2);
        assert_eq!(res[2].bytes[0], 0x65);
        assert_eq!(res[2].bytes[1], 0x05);
    }

    #[test]
    fn reference_outer_variables() {
        let res = just_bundles(
            r#"
    foo:
      rts

    .proc inner
        jsr foo
    .endproc
    "#,
        );

        assert_eq!(res.len(), 2);

        // foo: rts
        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0x60);

        // inner: jsr foo
        assert_eq!(res[1].size, 3);
        assert_eq!(res[1].bytes[0], 0x20);
        assert_eq!(res[1].bytes[1], 0x00);
        assert_eq!(res[1].bytes[2], 0x80);
    }

    #[test]
    fn bad_variable_but_valid_identifier_in_instruction() {
        assert_error(
            "adc Variable",
            1,
            false,
            "could not find variable 'Variable' in the global scope",
        );
        assert_error(
            "adc Scoped::Variable",
            1,
            false,
            "could not find scope 'Scoped'",
        );
    }

    #[test]
    fn redefined_variable() {
        assert_error(
            r#"
    .scope One
      Variable = 1
    .endscope

    Variable = 1
    Yet = 3
    Yet = 4
    "#,
            8,
            false,
            "'Yet' already defined in the global scope: you cannot re-assign names",
        );
    }

    #[test]
    fn trying_to_assign_on_relative_reference() {
        assert_error(
            "Var = :-",
            1,
            false,
            "trying to evaluate a relative reference as a bare value",
        );
    }

    #[test]
    fn unknown_variables() {
        assert_error(
            "lda #Variable",
            1,
            false,
            "'e' is not a decimal value and could not find variable \
                          'Variable' in the global scope either",
        );
        assert_error(
            "lda #Scope::Variable",
            1,
            false,
            "'e' is not a decimal value and could not find scope 'Scope' either",
        );
        assert_error(
            r#"
    .scope Scope
    .endscope
    lda #Scope::Variable
    "#,
            4,
            false,
            "'e' is not a decimal value and could not find variable 'Variable' in 'Scope' either",
        );
    }

    #[test]
    fn operations_with_variables() {
        let res = just_bundles(
            r#"
    Value = 4
    ldx #(2 + Value)
    ldx #(2 - Value)
    ldx #(2 * -Value)
    ldx #(Value / 2)
    ldx #(Value << 2)
    ldx #~Value
    ldx #(2 && Value)
    ldx #(2 || Value)
    ldx #(!Value)
    ldx #(2 == Value)
    ldx #(2 != Value)
    ldx #(2 <> Value)
    ldx #(2 < Value)
    ldx #(2 <= Value)
    ldx #(2 > Value)
    ldx #(2 >= Value)
    "#,
        );

        assert_eq!(res.len(), 16);

        let expected = [
            [0x06, 0x00], // Add
            [0xFE, 0xFF], // Subtract
            [0xF8, 0xFF], // Multiply
            [0x02, 0x00], // Divide
            [0x10, 0x00], // Left shift
            [0xFB, 0xFF], // Bitwise not
            [0x01, 0x00], // Logical and
            [0x01, 0x00], // Logical or
            [0x00, 0x00], // Logical not
            [0x00, 0x00], // Equal
            [0x01, 0x00], // Not equal
            [0x01, 0x00], // Not equal (bis)
            [0x01, 0x00], // Less
            [0x01, 0x00], // Less or equal
            [0x00, 0x00], // Greater
            [0x00, 0x00], // Greater or equal
        ];
        for (idx, node) in res.iter().enumerate() {
            assert_eq!(node.size, 2);
            assert_eq!(node.bytes[0], 0xA2);
            assert_eq!(node.bytes[1], expected[idx][0]);
            assert_eq!(node.bytes[2], expected[idx][1]);
        }
    }

    #[test]
    fn signed_with_variables() {
        let res = just_bundles(
            r#"
    Value = -2
    ldx #Value
    ldx #+Value
    "#,
        );

        assert_eq!(res.len(), 2);

        let neg = res.first().unwrap();
        assert_eq!(neg.size, 2);
        assert_eq!(neg.bytes[0], 0xA2);
        assert_eq!(neg.bytes[1], 0xFE);
        assert_eq!(neg.bytes[2], 0xFF);

        let pos = res.last().unwrap();
        assert_eq!(pos.size, 2);
        assert_eq!(pos.bytes[0], 0xA2);
        assert_eq!(pos.bytes[1], 0x02);
        assert_eq!(pos.bytes[2], 0x00);
    }

    #[test]
    fn asan_no_stack() {
        let line = "nop";
        let real_line = minimal_header().to_string() + line;

        let res = assemble_with_mapping(
            real_line.as_bytes(),
            empty(),
            &[],
            &SourceInfo::default(),
            true,
        );

        assert_eq!(res.warnings.len(), 1);
        assert_eq!(
            res.warnings[0].to_string(),
            "you have not defined a range for 'asan:stack'"
        );
    }

    #[test]
    fn asan_dup_stack() {
        let line = r#";; asan:stack $00-$FF
;; asan:stack $00-$FF
"#;

        let real_line = minimal_header().to_string() + line;

        let res = assemble_with_mapping(
            real_line.as_bytes(),
            empty(),
            &[],
            &SourceInfo::default(),
            true,
        );

        assert_eq!(res.errors.len(), 1);

        // NOTE: 3 lines for the "minimal header" being prepended.
        assert_eq!(
            res.errors[0].to_string(),
            "you have already defined a value for 'asan:stack' (line 5)"
        );
    }

    #[test]
    fn asan_stack() {
        let line = "nop ;; asan:stack $00-$FF";
        let real_line = minimal_header().to_string() + line;

        let res = assemble_with_mapping(
            real_line.as_bytes(),
            empty(),
            &[],
            &SourceInfo::default(),
            true,
        );

        assert_eq!(res.errors.len(), 0);
        assert_eq!(res.warnings.len(), 0);
        assert_eq!(res.memory.memory_ranges.len(), 1);

        let mr = res.memory.memory_ranges.first().unwrap();
        assert_eq!(mr.range, (0x100..0x200));
        assert_eq!(mr.name, "<stack>".to_string());

        assert_eq!(res.memory.total_internal_ram, 0x100);
    }

    #[test]
    fn asan_invalid_negative_variable() {
        let line = "m_var = -1";
        let real_line = minimal_header().to_string() + line;

        let res = assemble_with_mapping(
            real_line.as_bytes(),
            empty(),
            &[],
            &SourceInfo::default(),
            true,
        );

        assert!(res.bundles.is_empty());
        assert!(!res.errors.is_empty());

        // NOTE: 3 lines for the "minimal header" being prepended.
        assert_eq!(
            res.errors[0].to_string().as_str(),
            "you cannot use a negative value for a memory variable (line 4)"
        );
    }

    #[test]
    fn divide_by_zero() {
        assert_error(
            r#"Value = 0
    ldx #(2 / Value)
"#,
            2,
            false,
            "attempting to divide by zero",
        );
    }

    #[test]
    fn bad_shift() {
        assert_error(
            r#"Value = #$11
    ldx #(2 << Value)
    "#,
            2,
            false,
            "shift operator too big",
        );
    }

    #[test]
    fn reset_literal_mode_on_assignment() {
        let res = just_bundles(
            r#"var1 = $31
var2 = 20
cpx #(4 * var2)"#,
        );

        assert_eq!(res.len(), 1);
        assert_eq!(res[0].bytes[0], 0xE0);
        assert_eq!(res[0].bytes[1], 0x50);
        assert_eq!(res[0].bytes[2], 0x00);
    }

    // Regular instructions

    #[test]
    fn bad_addressing() {
        assert_error(
            "unknown #$20",
            1,
            false,
            "could not find a macro with the name 'unknown'",
        );
        assert_error(
            "adc ($2002, x)",
            1,
            false,
            "address can only be one byte long on indirect X addressing",
        );
        assert_error(
            "adc ($2002), y",
            1,
            false,
            "address can only be one byte long on indirect Y addressing",
        );
        assert_error(
            "adc ($20, y)",
            1,
            false,
            "only the X index is allowed on indirect X addressing",
        );
        assert_error(
            "adc ($20), x",
            1,
            false,
            "only the Y index is allowed on indirect Y addressing",
        );
        assert_error("adc $20, z", 1, false, "can only use X and Y as indices");
        assert_error(
            "adc ($2000)",
            1,
            false,
            "cannot use indirect addressing mode for the instruction 'adc'",
        );
        assert_error("lda //", 1, false, "invalid identifier");
        assert_error(
            "lda 12",
            1,
            false,
            "left arm of instruction is neither an address nor an immediate",
        );
        assert_error("adc #10000", 1, false, "immediate is too big");
    }

    #[test]
    fn adc() {
        assert_instruction("adc #20", &[0x69, 0x14]);
        assert_instruction("adc #$20", &[0x69, 0x20]);
        assert_instruction("adc $2002", &[0x6D, 0x02, 0x20]);
        assert_instruction("adc $20", &[0x65, 0x20]);
        assert_instruction("adc $20, x", &[0x75, 0x20]);
        assert_instruction("adc $2002, x", &[0x7D, 0x02, 0x20]);
        assert_instruction("adc $2002, y", &[0x79, 0x02, 0x20]);
        assert_instruction("adc ($20, x)", &[0x61, 0x20]);
        assert_instruction("adc ($20), y", &[0x71, 0x20]);
    }

    #[test]
    fn sbc() {
        assert_instruction("sbc #$20", &[0xE9, 0x20]);
        assert_instruction("sbc $2002", &[0xED, 0x02, 0x20]);
        assert_instruction("sbc $20", &[0xE5, 0x20]);
        assert_instruction("sbc $20, x", &[0xF5, 0x20]);
        assert_instruction("sbc $2002, x", &[0xFD, 0x02, 0x20]);
        assert_instruction("sbc $2002, y", &[0xF9, 0x02, 0x20]);
        assert_instruction("sbc ($20, x)", &[0xE1, 0x20]);
        assert_instruction("sbc ($20), y", &[0xF1, 0x20]);
    }

    #[test]
    fn shift() {
        // asl
        assert_instruction("asl", &[0x0A]);
        assert_instruction("asl a", &[0x0A]);
        assert_instruction("asl $20", &[0x06, 0x20]);
        assert_instruction("asl $20, x", &[0x16, 0x20]);
        assert_instruction("asl $2002", &[0x0E, 0x02, 0x20]);
        assert_instruction("asl $2002, x", &[0x1E, 0x02, 0x20]);

        // lsr
        assert_instruction("lsr", &[0x4A]);
        assert_instruction("lsr a", &[0x4A]);
        assert_instruction("lsr $20", &[0x46, 0x20]);
        assert_instruction("lsr $20, x", &[0x56, 0x20]);
        assert_instruction("lsr $2002", &[0x4E, 0x02, 0x20]);
        assert_instruction("lsr $2002, x", &[0x5E, 0x02, 0x20]);
    }

    #[test]
    fn rotate() {
        // rol
        assert_instruction("rol", &[0x2A]);
        assert_instruction("rol a", &[0x2A]);
        assert_instruction("rol $20", &[0x26, 0x20]);
        assert_instruction("rol $20, x", &[0x36, 0x20]);
        assert_instruction("rol $2002", &[0x2E, 0x02, 0x20]);
        assert_instruction("rol $2002, x", &[0x3E, 0x02, 0x20]);

        // ror
        assert_instruction("ror", &[0x6A]);
        assert_instruction("ror a", &[0x6A]);
        assert_instruction("ror $20", &[0x66, 0x20]);
        assert_instruction("ror $20, x", &[0x76, 0x20]);
        assert_instruction("ror $2002", &[0x6E, 0x02, 0x20]);
        assert_instruction("ror $2002, x", &[0x7E, 0x02, 0x20]);
    }

    #[test]
    fn and() {
        assert_instruction("and #$20", &[0x29, 0x20]);
        assert_instruction("and $2002", &[0x2D, 0x02, 0x20]);
        assert_instruction("and $20", &[0x25, 0x20]);
        assert_instruction("and $20, x", &[0x35, 0x20]);
        assert_instruction("and $2002, x", &[0x3D, 0x02, 0x20]);
        assert_instruction("and $2002, y", &[0x39, 0x02, 0x20]);
        assert_instruction("and ($20, x)", &[0x21, 0x20]);
        assert_instruction("and ($20), y", &[0x31, 0x20]);
    }

    #[test]
    fn or() {
        // eor
        assert_instruction("eor #$20", &[0x49, 0x20]);
        assert_instruction("eor $20", &[0x45, 0x20]);
        assert_instruction("eor $20, x", &[0x55, 0x20]);
        assert_instruction("eor $2002", &[0x4D, 0x02, 0x20]);
        assert_instruction("eor $2002, x", &[0x5D, 0x02, 0x20]);
        assert_instruction("eor $2002, y", &[0x59, 0x02, 0x20]);
        assert_instruction("eor ($20, x)", &[0x41, 0x20]);
        assert_instruction("eor ($20), y", &[0x51, 0x20]);

        // ora
        assert_instruction("ora #$20", &[0x09, 0x20]);
        assert_instruction("ora $20", &[0x05, 0x20]);
        assert_instruction("ora $20, x", &[0x15, 0x20]);
        assert_instruction("ora $2002", &[0x0D, 0x02, 0x20]);
        assert_instruction("ora $2002, x", &[0x1D, 0x02, 0x20]);
        assert_instruction("ora $2002, y", &[0x19, 0x02, 0x20]);
        assert_instruction("ora ($20, x)", &[0x01, 0x20]);
        assert_instruction("ora ($20), y", &[0x11, 0x20]);
    }

    #[test]
    fn load() {
        // lda
        assert_instruction("lda #$20", &[0xA9, 0x20]);
        assert_instruction("lda $20", &[0xA5, 0x20]);
        assert_instruction("lda $20, x", &[0xB5, 0x20]);
        assert_instruction("lda $2002", &[0xAD, 0x02, 0x20]);
        assert_instruction("lda $2002, x", &[0xBD, 0x02, 0x20]);
        assert_instruction("lda $2002, y", &[0xB9, 0x02, 0x20]);
        assert_instruction("lda ($20, x)", &[0xA1, 0x20]);
        assert_instruction("lda ($20), y", &[0xB1, 0x20]);

        // Expand zeropage-looking into absolute index.
        assert_instruction("lda $42, y", &[0xB9, 0x42, 0x00]);

        // ldx
        assert_instruction("ldx #$20", &[0xA2, 0x20]);
        assert_instruction("ldx $20", &[0xA6, 0x20]);
        assert_instruction("ldx $20, y", &[0xB6, 0x20]);
        assert_instruction("ldx $2002", &[0xAE, 0x02, 0x20]);
        assert_instruction("ldx $2002, y", &[0xBE, 0x02, 0x20]);

        // ldy
        assert_instruction("ldy #$20", &[0xA0, 0x20]);
        assert_instruction("ldy $20", &[0xA4, 0x20]);
        assert_instruction("ldy $20, x", &[0xB4, 0x20]);
        assert_instruction("ldy $2002", &[0xAC, 0x02, 0x20]);
        assert_instruction("ldy $2002, x", &[0xBC, 0x02, 0x20]);
    }

    #[test]
    fn jump() {
        assert_instruction("jsr $2002", &[0x20, 0x02, 0x20]);

        assert_instruction("jmp $2002", &[0x4C, 0x02, 0x20]);
        assert_instruction("jmp ($2002)", &[0x6C, 0x02, 0x20]);
        assert_instruction("jmp ($20)", &[0x6C, 0x20, 0x00]);
    }

    #[test]
    fn inc_dec_instructions() {
        // inc
        assert_instruction("inc $10", &[0xE6, 0x10]);
        assert_instruction("inc $1000", &[0xEE, 0x00, 0x10]);
        assert_instruction("inc $10, x", &[0xF6, 0x10]);
        assert_instruction("inc $1000, x", &[0xFE, 0x00, 0x10]);

        assert_instruction("inx", &[0xE8]);

        assert_instruction("iny", &[0xC8]);

        // dec
        assert_instruction("dec $10", &[0xC6, 0x10]);
        assert_instruction("dec $1000", &[0xCE, 0x00, 0x10]);
        assert_instruction("dec $10, x", &[0xD6, 0x10]);
        assert_instruction("dec $1000, x", &[0xDE, 0x00, 0x10]);

        assert_instruction("dex", &[0xCA]);

        assert_instruction("dey", &[0x88]);
    }

    #[test]
    fn transfer_instructions() {
        assert_instruction("tax", &[0xAA]);
        assert_instruction("tay", &[0xA8]);
        assert_instruction("tsx", &[0xBA]);
        assert_instruction("txa", &[0x8A]);
        assert_instruction("txs", &[0x9A]);
        assert_instruction("tya", &[0x98]);
    }

    #[test]
    fn return_instructions() {
        assert_instruction("rti", &[0x40]);
        assert_instruction("rts", &[0x60]);
    }

    #[test]
    fn set_clear_instructions() {
        assert_instruction("clc", &[0x18]);
        assert_instruction("cld", &[0xD8]);
        assert_instruction("cli", &[0x58]);
        assert_instruction("clv", &[0xB8]);

        assert_instruction("sec", &[0x38]);
        assert_instruction("sed", &[0xF8]);
        assert_instruction("sei", &[0x78]);
    }

    #[test]
    fn push_pull_instructions() {
        assert_instruction("pha", &[0x48]);
        assert_instruction("php", &[0x08]);
        assert_instruction("pla", &[0x68]);
        assert_instruction("plp", &[0x28]);
    }

    #[test]
    fn nop_brk() {
        assert_instruction("nop", &[0xEA]);
        assert_instruction("brk", &[0x00]);
    }

    #[test]
    fn cmp() {
        // cmp
        assert_instruction("cmp #$20", &[0xC9, 0x20]);
        assert_instruction("cmp $2002", &[0xCD, 0x02, 0x20]);
        assert_instruction("cmp $20", &[0xC5, 0x20]);
        assert_instruction("cmp $20, x", &[0xD5, 0x20]);
        assert_instruction("cmp $2002, x", &[0xDD, 0x02, 0x20]);
        assert_instruction("cmp $2002, y", &[0xD9, 0x02, 0x20]);
        assert_instruction("cmp ($20, x)", &[0xC1, 0x20]);
        assert_instruction("cmp ($20), y", &[0xD1, 0x20]);

        // cpx
        assert_instruction("cpx #$20", &[0xE0, 0x20]);
        assert_instruction("cpx $2002", &[0xEC, 0x02, 0x20]);
        assert_instruction("cpx $20", &[0xE4, 0x20]);

        // cpy
        assert_instruction("cpy #$20", &[0xC0, 0x20]);
        assert_instruction("cpy $2002", &[0xCC, 0x02, 0x20]);
        assert_instruction("cpy $20", &[0xC4, 0x20]);
    }

    #[test]
    fn store_instructions() {
        //sta
        assert_instruction("sta $20", &[0x85, 0x20]);
        assert_instruction("sta $20, x", &[0x95, 0x20]);
        assert_instruction("sta $2002", &[0x8D, 0x02, 0x20]);
        assert_instruction("sta $2002, x", &[0x9D, 0x02, 0x20]);
        assert_instruction("sta $2002, y", &[0x99, 0x02, 0x20]);
        assert_instruction("sta ($20, x)", &[0x81, 0x20]);
        assert_instruction("sta ($20), y", &[0x91, 0x20]);

        // Expand zeropage-looking into absolute index.
        assert_instruction("sta $42, y", &[0x99, 0x42, 0x00]);

        // stx
        assert_instruction("stx $20", &[0x86, 0x20]);
        assert_instruction("stx $20, y", &[0x96, 0x20]);
        assert_instruction("stx $2002", &[0x8E, 0x02, 0x20]);

        // sty
        assert_instruction("sty $20", &[0x84, 0x20]);
        assert_instruction("sty $20, x", &[0x94, 0x20]);
        assert_instruction("sty $2002", &[0x8C, 0x02, 0x20]);
    }

    #[test]
    fn bit() {
        assert_instruction("bit $10", &[0x24, 0x10]);
        assert_instruction("bit $1001", &[0x2C, 0x01, 0x10]);
    }

    #[test]
    fn arithmetic_memory_access() {
        let res = just_bundles(
            r#"
m_var = $200
sta $200 + 1
sta m_var + 1
    "#,
        );

        assert_eq!(res.len(), 2);

        // sta <absolute addressing> in both cases.

        assert_eq!(res[0].size, 3);
        assert_eq!(res[0].bytes[0], 0x8D);
        assert_eq!(res[0].bytes[1], 0x01);
        assert_eq!(res[0].bytes[2], 0x02);

        assert_eq!(res[1].size, 3);
        assert_eq!(res[1].bytes[0], 0x8D);
        assert_eq!(res[1].bytes[1], 0x01);
        assert_eq!(res[1].bytes[2], 0x02);
    }

    #[test]
    fn load_arithmetic_constant() {
        let res = just_bundles(
            r#"
VAL1 = 300
lda #(VAL1 - VAL2)
VAL2 = 260
    "#,
        );

        assert_eq!(res.len(), 1);

        assert_eq!(res[0].size, 2);
        assert_eq!(res[0].bytes[0], 0xA9);
        assert_eq!(res[0].bytes[1], 40);
        assert_eq!(res[0].bytes[2], 0x00);
    }

    // Labels & branching

    #[test]
    fn same_segment_labels() {
        let res = just_bundles(
            r#"
    nop
    @hello:
        jmp @hello
        jmp @end
    @end:
        nop
    "#,
        );

        assert_eq!(res.len(), 4);

        // jmp @hello
        assert_eq!(res[1].size, 3);
        assert_eq!(res[1].bytes[0], 0x4C);
        assert_eq!(res[1].bytes[1], 0x01);
        assert_eq!(res[1].bytes[2], 0x80);

        // jmp @end
        assert_eq!(res[2].size, 3);
        assert_eq!(res[2].bytes[0], 0x4C);
        assert_eq!(res[2].bytes[1], 0x07);
        assert_eq!(res[2].bytes[2], 0x80);
    }

    #[test]
    fn anonymous_relative_jumps() {
        let res = just_bundles(
            r#"
    nop
    :
        nop
    @hello:
        jmp :--
        jmp :+
        jmp @hello
        jmp :+++
    @end:
        nop
    :
        nop
    : nop
    "#,
        );

        assert_eq!(res.len(), 9);

        // First two nop's
        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0xEA);
        assert_eq!(res[1].size, 1);
        assert_eq!(res[1].bytes[0], 0xEA);

        // jmp :--
        assert_eq!(res[2].size, 3);
        assert_eq!(res[2].bytes[0], 0x4C);
        assert_eq!(res[2].bytes[1], 0x01);
        assert_eq!(res[2].bytes[2], 0x80);

        // jmp :+
        assert_eq!(res[3].size, 3);
        assert_eq!(res[3].bytes[0], 0x4C);
        assert_eq!(res[3].bytes[1], 0x0E);
        assert_eq!(res[3].bytes[2], 0x80);

        // jmp @hello
        assert_eq!(res[4].size, 3);
        assert_eq!(res[4].bytes[0], 0x4C);
        assert_eq!(res[4].bytes[1], 0x02);
        assert_eq!(res[4].bytes[2], 0x80);

        // jmp :+++
        assert_eq!(res[5].size, 3);
        assert_eq!(res[5].bytes[0], 0x4C);
        assert_eq!(res[5].bytes[1], 0x10);
        assert_eq!(res[5].bytes[2], 0x80);

        // Three last nop's.
        assert_eq!(res[6].size, 1);
        assert_eq!(res[6].bytes[0], 0xEA);
        assert_eq!(res[7].size, 1);
        assert_eq!(res[7].bytes[0], 0xEA);
        assert_eq!(res[8].size, 1);
        assert_eq!(res[8].bytes[0], 0xEA);
    }

    #[test]
    fn anonymous_relative_branches() {
        let res = just_bundles(
            r#"
    nop
    :
        nop
    @hello:
        beq :--
        beq :+
        beq @hello
        beq :+++
    @end:
        nop
    :
        nop
    : nop
    "#,
        );

        assert_eq!(res.len(), 9);

        // First two nop's
        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0xEA);
        assert_eq!(res[1].size, 1);
        assert_eq!(res[1].bytes[0], 0xEA);

        // beq :--
        assert_eq!(res[2].size, 2);
        assert_eq!(res[2].bytes[0], 0xF0);
        assert_eq!(res[2].bytes[1], 0xFD);

        // beq :+
        assert_eq!(res[3].size, 2);
        assert_eq!(res[3].bytes[0], 0xF0);
        assert_eq!(res[3].bytes[1], 0x04);

        // beq @hello
        assert_eq!(res[4].size, 2);
        assert_eq!(res[4].bytes[0], 0xF0);
        assert_eq!(res[4].bytes[1], 0xFA);

        // beq :+++
        assert_eq!(res[5].size, 2);
        assert_eq!(res[5].bytes[0], 0xF0);
        assert_eq!(res[5].bytes[1], 0x02);

        // Three last nop's.
        assert_eq!(res[6].size, 1);
        assert_eq!(res[6].bytes[0], 0xEA);
        assert_eq!(res[7].size, 1);
        assert_eq!(res[7].bytes[0], 0xEA);
        assert_eq!(res[8].size, 1);
        assert_eq!(res[8].bytes[0], 0xEA);
    }

    #[test]
    fn conditional_branch_to_labels() {
        let res = just_bundles(
            r#"
    nop
    @hello:
        beq @hello
        beq @end
    @end:
        nop
    "#,
        );

        assert_eq!(res.len(), 4);

        // beq @hello
        assert_eq!(res[1].size, 2);
        assert_eq!(res[1].bytes[0], 0xF0);
        assert_eq!(res[1].bytes[1], 0xFE);

        // beq @end
        assert_eq!(res[2].size, 2);
        assert_eq!(res[2].bytes[0], 0xF0);
        assert_eq!(res[2].bytes[1], 0x00);
    }

    #[test]
    fn jsr_to_proc() {
        let res = just_bundles(
            r#"nop
    .proc hello
      nop
      rts
    .endproc
      jsr hello
    "#,
        );

        assert_eq!(res.len(), 4);

        // First two nop's + rts
        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0xEA);
        assert_eq!(res[1].size, 1);
        assert_eq!(res[1].bytes[0], 0xEA);
        assert_eq!(res[2].size, 1);
        assert_eq!(res[2].bytes[0], 0x60);

        // jsr hello
        assert_eq!(res[3].size, 3);
        assert_eq!(res[3].bytes[0], 0x20);
        assert_eq!(res[3].bytes[1], 0x01);
        assert_eq!(res[3].bytes[2], 0x80);
    }

    #[test]
    fn label_in_instruction_addressing() {
        let res = just_bundles(
            r#"
      ldx #0
    @load_palettes_loop:
      lda palettes, x
    palettes:
      .byte $0F, $12, $22, $32
    "#,
        );

        assert_instruction("ldx #0", &res[0].bytes);
        assert_instruction("lda $8005, x", &res[1].bytes);
        assert_eq!(&res[2].bytes, &[0x0F, 0x00, 0x00]);
        assert_eq!(&res[3].bytes, &[0x12, 0x00, 0x00]);
        assert_eq!(&res[4].bytes, &[0x22, 0x00, 0x00]);
        assert_eq!(&res[5].bytes, &[0x32, 0x00, 0x00]);
    }

    #[test]
    fn label_in_instruction_addressing_with_arithmetics() {
        let res = just_bundles(
            r#"
      ldx #0
    @load_palettes_loop:
      lda palettes + 1, x
    palettes:
      .byte $0F, $12, $22, $32
    "#,
        );

        assert_instruction("ldx #0", &res[0].bytes);
        assert_instruction("lda $8006, x", &res[1].bytes);
        assert_eq!(&res[2].bytes, &[0x0F, 0x00, 0x00]);
        assert_eq!(&res[3].bytes, &[0x12, 0x00, 0x00]);
        assert_eq!(&res[4].bytes, &[0x22, 0x00, 0x00]);
        assert_eq!(&res[5].bytes, &[0x32, 0x00, 0x00]);
    }

    #[test]
    fn full_to_zeropage_optimization() {
        assert_instruction("sta $0020", &[0x85, 0x20]);
        assert_instruction("sty $021, x", &[0x94, 0x21]);
        assert_instruction("sta $002, x", &[0x95, 0x02]);
        assert_instruction("stx $020, y", &[0x96, 0x20]);
    }

    #[test]
    fn jsr_inside_multiple_contexts() {
        let res = just_bundles(
            r#".scope Movement
    .proc accelerate
        rts
    .endproc

    .proc update
        jsr accelerate
        rts
    .endproc
.endscope

jsr Movement::accelerate
jsr Movement::update
    "#,
        );

        assert_eq!(res.len(), 5);

        // accelerate -> rts
        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0x60);

        // jsr accelerate
        assert_eq!(res[1].size, 3);
        assert_eq!(res[1].bytes[0], 0x20);
        assert_eq!(res[1].bytes[1], 0x00);
        assert_eq!(res[1].bytes[2], 0x80);

        // update -> rts
        assert_eq!(res[2].size, 1);
        assert_eq!(res[2].bytes[0], 0x60);

        // jsr Movement::accelerate
        assert_eq!(res[3].size, 3);
        assert_eq!(res[3].bytes[0], 0x20);
        assert_eq!(res[3].bytes[1], 0x00);
        assert_eq!(res[3].bytes[2], 0x80);

        // jsr Movement::update
        assert_eq!(res[4].size, 3);
        assert_eq!(res[4].bytes[0], 0x20);
        assert_eq!(res[4].bytes[1], 0x01);
        assert_eq!(res[4].bytes[2], 0x80);
    }

    #[test]
    fn jmp_by_using_address_in_macro() {
        let res = just_bundles(
            r#"
.macro JAL ADDR
  jmp ADDR
.endmacro

JAL procedure

.proc procedure
  rts
.endproc
    "#,
        );

        assert_eq!(res.len(), 2);

        // JAL procedure
        assert_eq!(res[0].size, 3);
        assert_eq!(res[0].bytes[0], 0x4C);
        assert_eq!(res[0].bytes[1], 0x03);
        assert_eq!(res[0].bytes[2], 0x80);

        // rts
        assert_eq!(res[1].size, 1);
        assert_eq!(res[1].bytes[0], 0x60);
    }

    #[test]
    fn jump_to_next() {
        let res = just_assemble(
            r#"
.proc foo
  jmp bar
.endproc

.proc bar
  jsr another
.endproc

.proc another
  lda #0
  beq @next
@next:
  rts
.endproc
"#,
        );

        assert_eq!(res.warnings.len(), 3);
        // NOTE: +3 lines for the implicit header.
        assert_eq!(
            res.warnings[0].to_string(),
            "unconditional jump that points to the next instruction (line 6)"
        );
        assert_eq!(
            res.warnings[1].to_string(),
            "unconditional jump that points to the next instruction (line 10)"
        );
        assert_eq!(
            res.warnings[2].to_string(),
            "conditional jump that points to the next instruction (line 15)"
        );
    }

    // Control statements

    #[test]
    fn byte_literals() {
        let res = just_bundles(
            r#"
    .scope Vars
        Variable = 4
    .endscope

    .byte #Vars::Variable
    .dw $2001, $02
    .be $2001, $02
    "#,
        );

        assert_eq!(res.len(), 5);

        // .byte
        assert_eq!(res[0].bytes[0], 0x04);
        assert_eq!(res[0].bytes[1], 0x00);
        assert_eq!(res[0].size, 1);

        // First .dw argument.
        assert_eq!(res[1].bytes[0], 0x01);
        assert_eq!(res[1].bytes[1], 0x20);
        assert_eq!(res[1].size, 2);

        // Second .dw argument.
        assert_eq!(res[2].bytes[0], 0x02);
        assert_eq!(res[2].bytes[1], 0x00);
        assert_eq!(res[2].size, 2);

        // First .be argument.
        assert_eq!(res[3].bytes[0], 0x20);
        assert_eq!(res[3].bytes[1], 0x01);
        assert_eq!(res[3].size, 2);

        // Second .be argument.
        assert_eq!(res[4].bytes[0], 0x00);
        assert_eq!(res[4].bytes[1], 0x02);
        assert_eq!(res[4].size, 2);
    }

    #[test]
    fn reserve_memory() {
        let res = just_bundles(
            r#".res 2
.res 3, $02
.res 2, $00"#,
        );

        assert_eq!(res.len(), 7);

        assert_eq!(res[0].value(), 0);
        assert_eq!(res[1].value(), 0);
        assert_eq!(res[2].value(), 2);
        assert_eq!(res[3].value(), 2);
        assert_eq!(res[4].value(), 2);
        assert_eq!(res[5].value(), 0);
        assert_eq!(res[6].value(), 0);
    }

    #[test]
    fn bad_reserve_memory() {
        let mut res = just_assemble(".res 0, $00");

        assert_eq!(res.errors.len(), 1);
        assert_eq!(res.errors[0].to_string(), "empty .res statement (line 4)");

        res = just_assemble(".res 1, $00");
        assert_eq!(res.warnings.len(), 1);
        assert_eq!(
            res.warnings[0].to_string(),
            "pointless .res statement, prefer using .byte (line 4)"
        );
    }

    #[test]
    fn asciiz() {
        let res = just_bundles(".asciiz \"hello\"");

        assert_eq!(res.len(), 6);
        assert_eq!(res[0].value(), 0x68);
        assert_eq!(res[1].value(), 0x65);
        assert_eq!(res[2].value(), 0x6C);
        assert_eq!(res[3].value(), 0x6C);
        assert_eq!(res[4].value(), 0x6F);
        assert_eq!(res[5].value(), 0x00);
    }

    #[test]
    fn bad_asciiz() {
        let res = just_assemble(".asciiz \"\"");

        assert_eq!(res.errors.len(), 1);
        assert_eq!(
            res.errors[0].to_string(),
            "unpermitted empty argument (line 4)"
        );
    }

    #[test]
    fn hi_lo_byte() {
        let res = just_bundles(
            r#"
    Var = $2002
    lda #.lobyte(Var)
    lda #<Var
    lda #.hibyte(Var)
    lda #>Var
    lda #.hibyte(61953)
    "#,
        );

        assert_eq!(res.len(), 5);
        let instrs: Vec<[u8; 2]> = vec![
            [0xA9, 0x02],
            [0xA9, 0x02],
            [0xA9, 0x20],
            [0xA9, 0x20],
            [0xA9, 0xF2],
        ];

        for i in 0..instrs.len() {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn lobyte_with_arithmetic() {
        let res = just_bundles(
            r#"lda #.lobyte(@address - 1)
@address:
nop
    "#,
        );

        assert_eq!(res[0].bytes[0], 0xA9);
        assert_eq!(res[0].bytes[1], 0x01);
    }

    #[test]
    fn min_max() {
        let res = just_bundles(
            r#"
one = 1
two = 2

lda #.min(one, two, three)
lda #.max(one, two, three)

three = 3
    "#,
        );

        assert_eq!(res.len(), 2);
        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x01], [0xA9, 0x03]];

        for i in 0..instrs.len() {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn warnings_on_empty_proc_scope_macro() {
        let res = just_assemble(
            r#".proc Proc
    .endproc

    .scope Scope
    .endscope

    .macro MACRO
    .endmacro

    MACRO
    "#,
        );

        let warnings = res.warnings;
        assert_eq!(warnings.len(), 4);

        assert_eq!(warnings[0].to_string(), "empty .proc 'Proc' (line 4)");
        assert_eq!(warnings[1].to_string(), "empty .scope 'Scope' (line 7)");
        assert_eq!(
            warnings[2].to_string(),
            "trying to apply empty macro 'MACRO' (line 13)"
        );
        assert_eq!(warnings[3].to_string(), "segment 'CODE' is empty");
    }

    #[test]
    fn if_block() {
        let res = just_bundles(
            r#"Var = 0
.if Var == 0
  lda #1
.endif

.if Var == 1
  lda #1
.elsif Var == 0
  lda #2
.elsif Var == 2
  lda #3
.endif

.if Var == 1
  lda #1
.elsif Var == 2
  lda #2
.else
  lda #3
.endif
"#,
        );

        assert_eq!(res.len(), 3);

        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x01], [0xA9, 0x02], [0xA9, 0x03]];
        for i in 0..3 {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn bad_if() {
        assert_error(
            r#"Var = 1
.if Var == 1
  lda #1
    "#,
            5,
            false,
            "expecting a 'control function (.endif)' but there are no more statements",
        );

        let res = just_assemble(
            r#"Var = 1
;;;.if Var1 == 1
  lda #1
.elsif Var1 == 2
  lda #2
.else
  lda #3
.endif
    "#,
        );

        assert_eq!(res.errors.len(), 3);

        assert_eq!(res.errors[0].line, 6);
        assert_eq!(
            res.errors[0].message,
            "unexpected 'control function (.elsif)'"
        );
        assert_eq!(res.errors[1].line, 8);
        assert_eq!(
            res.errors[1].message,
            "unexpected 'control function (.else)'"
        );
        assert_eq!(res.errors[2].line, 10);
        assert_eq!(
            res.errors[2].message,
            "unexpected 'control function (.endif)'"
        );
    }

    #[test]
    fn defined() {
        let res = just_bundles(
            r#"Var = 0
.if .defined(Var)
  lda #1
.else
  lda #0
.endif

.if .defined(Var1)
  lda #1
.else
  lda #0
.endif

.if !.defined(Var1)
  lda #1
.else
  lda #0
.endif
"#,
        );

        assert_eq!(res.len(), 3);

        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x01], [0xA9, 0x00], [0xA9, 0x01]];
        for i in 0..instrs.len() {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn ifdef_block() {
        let res = just_bundles(
            r#"Var = 0
.ifdef Var
  lda #1
.endif

.ifndef Var
  lda #1
.elsif Var == 0
  lda #2
.elsif Var == 2
  lda #3
.endif
"#,
        );

        assert_eq!(res.len(), 2);

        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x01], [0xA9, 0x02]];
        for i in 0..instrs.len() {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn dont_redefine_variables_inside_false_ifdefs() {
        let res = just_bundles(
            r#"Var = 1
.ifndef Var
  Var = 0
.endif

.if !.defined(Var)
  Var = 0
.endif

lda #Var
"#,
        );

        assert_eq!(res.len(), 1);

        assert_eq!(res[0].size, 2);
        assert_eq!(res[0].bytes[0], 0xA9);
        assert_eq!(res[0].bytes[1], 0x01);
    }

    // Macros

    #[test]
    fn macro_no_arguments() {
        let res = just_bundles(
            r#"
    lda #42

    .macro MACRO
        lda #2
    .endmacro

    lda #1
    MACRO
    "#,
        );

        assert_eq!(res.len(), 3);
        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x2A], [0xA9, 0x01], [0xA9, 0x02]];

        for i in 0..3 {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn macro_not_enough_arguments() {
        assert_error(
            r#"
    lda #42

    .macro MACRO(Var)
        lda #Var
    .endmacro

    lda #1
    MACRO
    "#,
            9,
            false,
            "wrong number of arguments for 'MACRO': 1 required but 0 given",
        );
    }

    #[test]
    fn macro_too_many_arguments() {
        assert_error(
            r#"
    lda #42

    .macro MACRO(Var)
        lda #Var
    .endmacro

    lda #1
    MACRO(1, 2)
    "#,
            9,
            false,
            "wrong number of arguments for 'MACRO': 1 required but 2 given",
        );
    }

    #[test]
    fn macro_with_one_argument() {
        let res = just_bundles(
            r#"
    lda #42

    .macro MACRO(Var)
        lda #Var
    .endmacro

    lda #1
    MACRO(2)
    "#,
        );

        assert_eq!(res.len(), 3);
        let instrs: Vec<[u8; 2]> = vec![[0xA9, 0x2A], [0xA9, 0x01], [0xA9, 0x02]];

        for i in 0..3 {
            assert_eq!(res[i].size, 2);
            assert_eq!(res[i].bytes[0], instrs[i][0]);
            assert_eq!(res[i].bytes[1], instrs[i][1]);
        }
    }

    #[test]
    fn macro_variable_argument() {
        let res = just_bundles(
            r#".macro MACRO arg
    lda arg
.endmacro

Var1 = $20

MACRO Var1
"#,
        );

        assert_eq!(res.len(), 1);
        assert_eq!(res[0].size, 2);
        assert_eq!(res[0].bytes[0], 0xA5);
        assert_eq!(res[0].bytes[1], 0x20);
    }

    #[test]
    fn macro_unknown_arguments() {
        assert_error(
            r#"
    lda #42

    .macro MACRO(Var)
        lda #Va
    .endmacro

    lda #1
    MACRO(1)
    "#,
            5,
            false,
            "'a' is not a decimal value and \
                 could not find variable 'Va' in the global scope either",
        );
    }

    #[test]
    fn macro_multiple_arguments() {
        let res = just_bundles(
            r#"
    .macro WRITE_PPU_DATA address, value
        bit $2002                   ; PPUSTATUS
        lda #.HIBYTE(address)
        sta $2006                   ; PPUADDR
        lda #.LOBYTE(address)
        sta $2006                   ; PPUADDR
        lda #value
        sta $2007                   ; PPUDATA
    .endmacro

    WRITE_PPU_DATA $20B9, $04
    "#,
        );

        assert_eq!(res.len(), 7);

        // bit $2002
        assert_eq!(res[0].size, 3);
        assert_eq!(res[0].bytes[0], 0x2C);
        assert_eq!(res[0].bytes[1], 0x02);
        assert_eq!(res[0].bytes[2], 0x20);

        // lda #.HIBYTE(address)
        assert_eq!(res[1].size, 2);
        assert_eq!(res[1].bytes[0], 0xA9);
        assert_eq!(res[1].bytes[1], 0x20);

        // sta $2006
        assert_eq!(res[2].size, 3);
        assert_eq!(res[2].bytes[0], 0x8D);
        assert_eq!(res[2].bytes[1], 0x06);
        assert_eq!(res[2].bytes[2], 0x20);

        // lda #.LOBYTE(address)
        assert_eq!(res[3].size, 2);
        assert_eq!(res[3].bytes[0], 0xA9);
        assert_eq!(res[3].bytes[1], 0xB9);

        // sta $2006
        assert_eq!(res[4].size, 3);
        assert_eq!(res[4].bytes[0], 0x8D);
        assert_eq!(res[4].bytes[1], 0x06);
        assert_eq!(res[4].bytes[2], 0x20);

        // lda #value
        assert_eq!(res[5].size, 2);
        assert_eq!(res[5].bytes[0], 0xA9);
        assert_eq!(res[5].bytes[1], 0x04);

        // sta $2007
        assert_eq!(res[6].size, 3);
        assert_eq!(res[6].bytes[0], 0x8D);
        assert_eq!(res[6].bytes[1], 0x07);
        assert_eq!(res[6].bytes[2], 0x20);
    }

    #[test]
    fn start_macro_inside_of_scope() {
        assert_error(
            r#".scope Some
    .macro WRITE_PPU_DATA
        bit $2002
    .endmacro
    .endscope
    "#,
            2,
            false,
            ".macro must be on the global scope",
        );
    }

    #[test]
    fn macro_call_inside_of_proc() {
        let res = just_bundles(
            r#".macro MACRO
    nop
    .endmacro

    .proc Foo
      MACRO
    .endproc
    "#,
        );

        assert_eq!(res.len(), 1);

        assert_eq!(res[0].size, 1);
        assert_eq!(res[0].bytes[0], 0xEA);
        assert_eq!(res[0].bytes[1], 0x00);
        assert_eq!(res[0].bytes[2], 0x00);
    }

    #[test]
    fn bad_macro_name() {
        assert_error(
            r#".macro __fallthrough__ arg
    .endmacro
    "#,
            1,
            false,
            "'__fallthrough__' is not a valid macro name: cannot use reserved name '__fallthrough__'",
        );
    }

    #[test]
    fn bad_scope_definition_inside_of_proc() {
        assert_error(
            r#".proc Hey
    .scope Something
    .endscope
    .endproc
    "#,
            2,
            false,
            "you cannot call '.scope' in this context",
        );
    }

    #[test]
    fn bad_scope_definition_inside_of_macro() {
        assert_error(
            r#".macro Hey
    .scope Something
    .endscope
    .endmacro
    "#,
            2,
            false,
            "you cannot call '.scope' in this context",
        );
    }

    #[test]
    fn bad_proc_definition_inside_of_proc() {
        assert_error(
            r#".proc Hey
    .proc Something
    .endproc
    .endproc
    "#,
            2,
            false,
            "you cannot call '.proc' in this context",
        );
    }

    #[test]
    fn bad_proc_definition_inside_of_macro() {
        assert_error(
            r#".macro Hey
    .proc Something
    .endproc
    .endmacro
    "#,
            2,
            false,
            "you cannot call '.proc' in this context",
        );
    }

    #[test]
    fn error_on_named_label_inside_macro() {
        assert_error(
            r#".macro MACRO
    @label:
      jmp @label
    .endmacro
    "#,
            2,
            false,
            "using a named label ('@label') inside of a macro/repeat definition",
        );
    }

    #[test]
    fn jumps_inside_of_macros() {
        let res = just_bundles(
            r#".macro MACRO address, value
        lda #.HIBYTE(address)
        lda #.LOBYTE(address)
    :
        lda #value
        beq :-
    .endmacro

    MACRO $20B9, $04
    "#,
        );

        assert_eq!(res.len(), 4);

        // lda #.HIBYTE(address)
        assert_eq!(res[0].size, 2);
        assert_eq!(res[0].bytes[0], 0xA9);
        assert_eq!(res[0].bytes[1], 0x20);

        // lda #.LOBYTE(address)
        assert_eq!(res[1].size, 2);
        assert_eq!(res[1].bytes[0], 0xA9);
        assert_eq!(res[1].bytes[1], 0xB9);

        // lda #value
        assert_eq!(res[2].size, 2);
        assert_eq!(res[2].bytes[0], 0xA9);
        assert_eq!(res[2].bytes[1], 0x04);

        // beq :-
        assert_eq!(res[3].size, 2);
        assert_eq!(res[3].bytes[0], 0xF0);
        assert_eq!(res[3].bytes[1], 0xFC);
    }

    #[test]
    fn arithmetic_on_macro_arg() {
        let res = just_bundles(
            r#"
.macro MC ADDR
    adc ADDR, y
.endmacro

MC things + 1

things:
    .byte $00, $01
    "#,
        );

        assert_eq!(res.len(), 3);

        assert_eq!(res[0].size, 3);
        assert_eq!(res[0].bytes[0], 0x79);
        assert_eq!(res[0].bytes[1], 0x04);
        assert_eq!(res[0].bytes[2], 0x80);
    }

    // .repeat

    #[test]
    fn code_gets_repeated() {
        let res = just_bundles(
            r#".repeat 3
nop
.endrepeat
"#,
        );

        assert_eq!(res.len(), 3);

        for it in res.iter().take(2) {
            assert_eq!(it.size, 1);
            assert_eq!(it.bytes[0], 0xEA);
            assert_eq!(it.bytes[1], 0x00);
            assert_eq!(it.bytes[2], 0x00);
        }
    }

    #[test]
    fn variable_in_repeat() {
        let res = just_bundles(
            r#".repeat 2, I
.repeat 2, J
lda #I
ldx #J
.endrepeat
.endrepeat"#,
        );

        assert_eq!(res.len(), 8);

        // 00
        assert_eq!(res[0].bytes[0], 0xA9);
        assert_eq!(res[0].bytes[1], 0x00);
        assert_eq!(res[1].bytes[0], 0xA2);
        assert_eq!(res[1].bytes[1], 0x00);

        // 01
        assert_eq!(res[2].bytes[0], 0xA9);
        assert_eq!(res[2].bytes[1], 0x00);
        assert_eq!(res[3].bytes[0], 0xA2);
        assert_eq!(res[3].bytes[1], 0x01);

        // 10
        assert_eq!(res[4].bytes[0], 0xA9);
        assert_eq!(res[4].bytes[1], 0x01);
        assert_eq!(res[5].bytes[0], 0xA2);
        assert_eq!(res[5].bytes[1], 0x00);

        // 11
        assert_eq!(res[6].bytes[0], 0xA9);
        assert_eq!(res[6].bytes[1], 0x01);
        assert_eq!(res[7].bytes[0], 0xA2);
        assert_eq!(res[7].bytes[1], 0x01);
    }

    #[test]
    fn error_on_invalid_repeat() {
        assert_error(
            ".repeat 1\n.endrepeat",
            1,
            false,
            "pointless .repeat statement",
        );
        assert_error(
            ".repeat a\n.endrepeat",
            1,
            false,
            "first argument must be an integer, 'a' found instead",
        );
        assert_error(
            ".repeat 256\n.endrepeat",
            1,
            false,
            "the number of iterations has to fit in a single byte",
        );
    }

    #[test]
    fn warning_on_empty_repeat() {
        let res = just_assemble(".repeat 2\n.endrepeat");

        assert!(res.bundles[0x10..].is_empty());
        assert!(res.errors.is_empty());

        assert_eq!(res.warnings.len(), 2); // empty code and the one we are testing.
        assert_eq!(
            res.warnings.first().unwrap().to_string(),
            "empty .repeat statement (line 4)"
        );
    }

    #[test]
    fn custom_human_message_on_unknown_variable_in_repeat() {
        assert_error(
            r#".repeat 2
lda #Variable
.endrepeat
"#,
            2,
            false,
            "'e' is not a decimal value and could not find variable 'Variable' \
             in the current scope either",
        );
    }

    // Segments

    #[test]
    fn error_on_unknown_segment() {
        assert_error(
            r#"
    .segment "THREE"

    .segment "TWO"
    nop
    "#,
            2,
            false,
            "unknown segment 'THREE'",
        );
    }

    #[test]
    fn warning_on_empty_segment() {
        let res = assemble_with_mapping(
            r#"
.segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

    .segment "ONE"

    .segment "TWO"
    nop
    "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        assert_eq!(res.bundles.len(), 0x11);

        let warnings = res.warnings;
        assert_eq!(warnings.len(), 1);
        assert_eq!(
            warnings.first().unwrap().to_string(),
            "segment 'ONE' is empty"
        );
    }

    #[test]
    fn jmp_and_beq_inside_segment() {
        let res = assemble_with_mapping(
            r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

    .segment "ONE"
    nop

    .segment "TWO"
    nop
    :
        nop
    @hello:
        beq :--
        beq :+
        jmp @hello
        beq @hello
        beq :+++
        beq @end
    @end:
        nop
    :
        nop
    : nop
    "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        let bundles = &res.bundles[0x11..];

        // First two nop's
        assert_eq!(bundles[0].size, 1);
        assert_eq!(bundles[0].bytes[0], 0xEA);
        assert_eq!(bundles[1].size, 1);
        assert_eq!(bundles[1].bytes[0], 0xEA);

        // beq :--
        assert_eq!(bundles[2].size, 2);
        assert_eq!(bundles[2].bytes[0], 0xF0);
        assert_eq!(bundles[2].bytes[1], 0xFD);

        // beq :+
        assert_eq!(bundles[3].size, 2);
        assert_eq!(bundles[3].bytes[0], 0xF0);
        assert_eq!(bundles[3].bytes[1], 0x09);

        // jmp @hello
        assert_eq!(bundles[4].size, 3);
        assert_eq!(bundles[4].bytes[0], 0x4C);
        assert_eq!(bundles[4].bytes[1], 0x03);
        assert_eq!(bundles[4].bytes[2], 0x80);

        // beq @hello
        assert_eq!(bundles[5].size, 2);
        assert_eq!(bundles[5].bytes[0], 0xF0);
        assert_eq!(bundles[5].bytes[1], 0xF7);

        // beq :+++
        assert_eq!(bundles[6].size, 2);
        assert_eq!(bundles[6].bytes[0], 0xF0);
        assert_eq!(bundles[6].bytes[1], 0x04);

        // beq @end
        assert_eq!(bundles[7].size, 2);
        assert_eq!(bundles[7].bytes[0], 0xF0);
        assert_eq!(bundles[7].bytes[1], 0x00);

        // Three last nop's.
        assert_eq!(bundles[8].size, 1);
        assert_eq!(bundles[8].bytes[0], 0xEA);
        assert_eq!(bundles[9].size, 1);
        assert_eq!(bundles[9].bytes[0], 0xEA);
        assert_eq!(bundles[10].size, 1);
        assert_eq!(bundles[10].bytes[0], 0xEA);
    }

    #[test]
    fn jmp_on_different_segments_intertwined() {
        let res = assemble_with_mapping(
            r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

    .segment "ONE"
    lala:
        rts
        .byte $F3

    .segment "TWO"
    code:
        jsr lala
        jsr code
        rts

    .segment "ONE"
        jsr code
    "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        let bundles = &res.bundles[0x12..]; // Ignoring HEADER + first two ONE

        // "jsr code" from ONE (notice that it's intertwined!)
        assert_eq!(bundles[0].size, 3);
        assert_eq!(bundles[0].bytes[0], 0x20);
        assert_eq!(bundles[0].bytes[1], 0x05);
        assert_eq!(bundles[0].bytes[2], 0x80);

        // "jsr lala" from TWO
        assert_eq!(bundles[1].size, 3);
        assert_eq!(bundles[1].bytes[0], 0x20);
        assert_eq!(bundles[1].bytes[1], 0x00);
        assert_eq!(bundles[1].bytes[2], 0x80);

        // "jsr code" from TWO (again, notice that it's intertwined)
        assert_eq!(bundles[2].size, 3);
        assert_eq!(bundles[2].bytes[0], 0x20);
        assert_eq!(bundles[2].bytes[1], 0x05);
        assert_eq!(bundles[2].bytes[2], 0x80);
    }

    #[test]
    fn jumps_and_labels_inside_proc() {
        let res = assemble_with_mapping(
            r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

.segment "ONE"
    nop
    .segment "TWO"
    .proc Foo
        lda #$10
    :
        beq :-
        jmp @label
        nop
    @label:
        rts
    .endproc

    jsr Foo
    "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        let bundles = &res.bundles[0x11..]; // Ignoring HEADER + first nop

        assert_eq!(bundles.len(), 6);

        // lda #$10
        assert_eq!(bundles[0].size, 2);
        assert_eq!(bundles[0].bytes[0], 0xA9);
        assert_eq!(bundles[0].bytes[1], 0x10);

        // beq :-
        assert_eq!(bundles[1].size, 2);
        assert_eq!(bundles[1].bytes[0], 0xF0);
        assert_eq!(bundles[1].bytes[1], 0xFE);

        // jmp @label
        assert_eq!(bundles[2].size, 3);
        assert_eq!(bundles[2].bytes[0], 0x4C);
        assert_eq!(bundles[2].bytes[1], 0x09);
        assert_eq!(bundles[2].bytes[2], 0x80);

        // nop
        assert_eq!(bundles[3].size, 1);
        assert_eq!(bundles[3].bytes[0], 0xEA);

        // rts
        assert_eq!(bundles[4].size, 1);
        assert_eq!(bundles[4].bytes[0], 0x60);

        // jsr Foo
        assert_eq!(bundles[5].size, 3);
        assert_eq!(bundles[5].bytes[0], 0x20);
        assert_eq!(bundles[5].bytes[1], 0x01);
        assert_eq!(bundles[5].bytes[2], 0x80);
    }

    #[test]
    fn proc_reference_another_segment() {
        let res = assemble_with_mapping(
            r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

    .segment "ONE"
    .addr code

    .segment "TWO"
    nop
    code:
        rts
    "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        let bundles = &res.bundles[0x10..];

        assert_eq!(bundles[0].size, 2);
        assert_eq!(bundles[0].bytes[0], 0x03);
        assert_eq!(bundles[0].bytes[1], 0x80);
    }

    #[test]
    fn cannot_fit_address_in_one_byte() {
        let res = assemble_with_mapping(
            r#".segment "HEADER"
.byte 'N', 'E', 'S', $1A, $01, $00

.segment "ONE"
        .byte code

        .segment "TWO"
        nop
        code:
            rts
        "#
            .as_bytes(),
            one_two().to_vec(),
            &[],
            &SourceInfo::default(),
            false,
        );

        assert_eq!(
            res.errors[0].to_string(),
            "expecting an argument that fits into a byte (line 5)"
        );
    }

    #[test]
    fn cannot_switch_to_segment_inside_of_scope() {
        assert_error(
            r#"
        .scope Vars
        .segment "CODE"
        nop
        .endscope
        "#,
            3,
            false,
            ".segment must be on the global scope",
        );
    }
}
