use alloc::{
    collections::BTreeSet,
    string::{String, ToString},
    vec::Vec,
};
use core::str::FromStr;
use std::{
    eprintln,
    sync::{Arc, LazyLock},
};

use miden_assembly_syntax::{
    MAX_REPEAT_COUNT, ast::Path, diagnostics::WrapErr, library::LibraryExport,
};
use miden_core::{
    Felt, Word, assert_matches,
    events::EventId,
    field::PrimeField64,
    mast::{
        CallNodeBuilder, JoinNodeBuilder, LoopNodeBuilder, MastForestContributor, MastNodeExt,
        MastNodeId, SplitNodeBuilder,
    },
    operations::Operation,
    program::Program,
    serde::{Deserializable, Serializable},
};
use miden_mast_package::{
    ConstantExport, MastForest, Package, PackageExport, PackageId, PackageManifest,
    ProcedureExport, TargetType, TypeExport,
};
use proptest::{
    prelude::*,
    test_runner::{Config, TestRunner},
};

use crate::{
    Assembler, Library, ModuleParser, PathBuf, ProjectSourceInputs, ProjectTargetSelector,
    assembler::MAX_CONTROL_FLOW_NESTING,
    ast::{Module, ModuleKind, ProcedureName, QualifiedProcedureName},
    diagnostics::{IntoDiagnostic, Report},
    fmp::fmp_initialization_sequence,
    mast_forest_builder::MastForestBuilder,
    report,
    testing::{
        TestContext, assert_diagnostic, assert_diagnostic_lines, parse_module, regex, source_file,
    },
};

type TestResult = Result<(), Report>;

// Note: where possible, prefer insta to pretty_assertions for snapshot testing.
//
// - For tests against expected values that can't be expressed as a string literal, we still use
//   pretty-assertions' assert_eq for backward compatibility.
// - For new tests using string literals, or when you need auto-updating snapshots, use [insta](https://insta.rs/):
//
// Example:
// insta::assert_snapshot!(actual_output)
//
// To update snapshots on a per-test basis automatically:
// cargo insta review (after `cargo install cargo-insta`)

macro_rules! assert_assembler_diagnostic {
    ($context:ident, $source:expr, $($expected:literal),+) => {{
        let error = $context
            .assemble($source)
            .expect_err("expected diagnostic to be raised, but compilation succeeded");
        assert_diagnostic_lines!(error, $($expected),*);
    }};

    ($context:ident, $source:expr, $($expected:expr),+) => {{
        let error = $context
            .assemble($source)
            .expect_err("expected diagnostic to be raised, but compilation succeeded");
        assert_diagnostic_lines!(error, $($expected),*);
    }};
}

// SIMPLE PROGRAMS
// ================================================================================================

#[test]
fn simple_instructions() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin push.0 assertz end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    let source = source_file!(&context, "begin push.10 push.50 push.2 u32wrapping_madd end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    let source = source_file!(&context, "begin push.10 push.50 push.2 u32wrapping_add3 end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

/// TODO(pauls): Do we want to allow this in Miden Assembly?
#[test]
#[ignore]
fn empty_program() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn empty_if() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin if.true end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:15\]"#),
        "1 | begin if.true end end",
        "  :               ^|^",
        "  :                `-- found a end here",
        "  `----",
        " help: expected primitive opcode (e.g. \"add\"), or \"else\", or control flow opcode (e.g. \"if.true\")"
    );
    Ok(())
}

#[test]
fn empty_if_true_then_branch() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin if.true nop end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

/// TODO(pauls): Do we want to allow this in Miden Assembly
#[test]
#[ignore]
fn empty_while() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin while.true end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

/// TODO(pauls): Do we want to allow this in Miden Assembly
#[test]
#[ignore]
fn empty_repeat() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin repeat.5 end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

/// This test ensures that all iterations of a repeat control block are merged into a single basic
/// block.
#[test]
fn repeat_basic_blocks_merged() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin mul repeat.5 add end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    // Also ensure that dead code elimination works properly
    assert_eq!(program.mast_forest().num_nodes(), 1);
    Ok(())
}

/// Ensures `repeat` supports dynamic iteration counts provided via constants.
#[test]
fn repeat_dynamic_iteration_count() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "const A = 5 begin repeat.A add end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn single_basic_block() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin push.1 push.2 add end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn basic_block_and_simple_if_true() -> TestResult {
    let context = TestContext::default();

    // if with else
    let source = source_file!(&context, "begin push.2 push.3 if.true add else mul end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    // if without else
    let source = source_file!(&context, "begin push.2 push.3 if.true add end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn basic_block_and_simple_if_false() -> TestResult {
    let context = TestContext::default();

    // if with else
    let source = source_file!(&context, "begin push.2 push.3 if.false add else mul end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    // if without else
    let source = source_file!(&context, "begin push.2 push.3 if.false add end end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

// LIBRARIES
// ================================================================================================

#[test]
fn library_exports() -> Result<(), Report> {
    let context = TestContext::new();

    // build the first library
    let baz = r#"
        pub proc baz1
            push.7 push.8 sub
        end
    "#;
    let baz = parse_module!(&context, "lib1::baz", baz);

    let lib1 = Assembler::new(context.source_manager()).assemble_library([baz])?;

    // build the second library
    let foo = r#"
        proc foo1
            push.1 add
        end

        pub proc foo2
            push.2 add
            exec.foo1
        end

        pub proc foo3
            push.3 mul
            exec.foo1
            exec.foo2
        end
    "#;
    let foo = parse_module!(&context, "lib2::foo", foo);

    // declare bar module
    let bar = r#"
        use lib1::baz
        use lib2::foo

        pub use baz::baz1->bar1

        pub use foo::foo2->bar2

        pub proc bar3
            exec.foo::foo2
        end

        proc bar4
            push.1 push.2 mul
        end

        pub proc bar5
            push.3 sub
            exec.foo::foo2
            exec.bar1
            exec.bar2
            exec.bar4
        end
    "#;
    let bar = parse_module!(&context, "lib2::bar", bar);
    let lib2_modules = [foo, bar];

    let lib2 = Assembler::new(context.source_manager())
        .with_dynamic_library(lib1)?
        .assemble_library(lib2_modules.iter().cloned())?;

    let foo2 = Path::new("::lib2::foo::foo2");
    let foo3 = Path::new("::lib2::foo::foo3");
    let bar1 = Path::new("::lib2::bar::bar1");
    let bar2 = Path::new("::lib2::bar::bar2");
    let bar3 = Path::new("::lib2::bar::bar3");
    let bar5 = Path::new("::lib2::bar::bar5");

    // make sure the library exports all exported procedures
    let expected_exports: BTreeSet<Arc<Path>> =
        [foo2.into(), foo3.into(), bar1.into(), bar2.into(), bar3.into(), bar5.into()].into();
    let actual_exports: BTreeSet<_> = lib2.exports().map(|export| export.path()).collect();
    assert_eq!(expected_exports, actual_exports);

    // make sure foo2, bar2, and bar3 map to the same MastNode
    assert_eq!(lib2.get_export_node_id(foo2), lib2.get_export_node_id(bar2));
    assert_eq!(lib2.get_export_node_id(foo2), lib2.get_export_node_id(bar3));

    // make sure there are 6 roots in the MAST (foo1, foo2, foo3, bar1, bar4, and bar5)
    assert_eq!(lib2.mast_forest().num_procedures(), 6);

    // bar1 should be the only re-export (i.e. the only procedure re-exported from a dependency)
    assert!(!lib2.is_reexport(foo2));
    assert!(!lib2.is_reexport(foo3));
    assert!(lib2.is_reexport(bar1));
    assert!(!lib2.is_reexport(bar2));
    assert!(!lib2.is_reexport(bar3));
    assert!(!lib2.is_reexport(bar5));

    Ok(())
}

#[test]
fn library_procedure_collision() -> Result<(), Report> {
    let context = TestContext::new();

    // build the first library
    let foo = r#"
        pub proc foo1
            push.1
            if.true
                push.1 push.2 add
            else
                push.1 push.2 mul
            end
        end
    "#;
    let foo = parse_module!(&context, "lib1::foo", foo);
    let lib1 = Assembler::new(context.source_manager()).assemble_library([foo])?;

    // build the second library which defines the same procedure as the first one
    let bar = r#"
        use lib1::foo

        pub use foo::foo1->bar1

        pub proc bar2
            push.1
            if.true
                push.1 push.2 add
            else
                push.1 push.2 mul
            end
        end
    "#;
    let bar = parse_module!(&context, "lib2::bar", bar);
    let lib2 = Assembler::new(context.source_manager())
        .with_dynamic_library(lib1)?
        .assemble_library([bar])?;

    // make sure lib2 has the expected exports (i.e., bar1 and bar2)
    assert_eq!(lib2.num_exports(), 2);

    // AssemblyOp metadata now participates in assembler-side deduplication, so a re-exported
    // procedure and a locally defined procedure with the same operations remain distinct when
    // their source mappings differ.
    let lib2_bar_bar1 = QualifiedProcedureName::from_str("lib2::bar::bar1").unwrap();
    let lib2_bar_bar2 = QualifiedProcedureName::from_str("lib2::bar::bar2").unwrap();
    assert_ne!(lib2.get_export_node_id(&lib2_bar_bar1), lib2.get_export_node_id(&lib2_bar_bar2));

    // Keeping those procedures distinct adds one more node to the library forest.
    assert_eq!(lib2.mast_forest().num_nodes(), 6);

    Ok(())
}

#[test]
fn library_serialization() -> Result<(), Report> {
    let context = TestContext::new();
    // declare foo module
    let foo = r#"
        pub proc foo
            add
        end
        pub proc foo_mul
            mul
        end
    "#;
    let foo = parse_module!(&context, "test::foo", foo);

    // declare bar module
    let bar = r#"
        pub proc bar
            mtree_get
        end
        pub proc bar_mul
            mul
        end
    "#;
    let bar = parse_module!(&context, "test::bar", bar);
    let modules = [foo, bar];

    // serialize/deserialize the bundle with locations
    let bundle =
        Assembler::new(context.source_manager()).assemble_library(modules.iter().cloned())?;

    let bytes = bundle.to_bytes();
    let deserialized = Library::read_from_bytes(&bytes).unwrap();
    assert_eq!(bundle.as_ref(), &deserialized);

    Ok(())
}

#[test]
fn get_module_by_path() -> Result<(), Report> {
    let context = TestContext::new();
    // declare foo module
    let foo_source = r#"
        pub proc foo
            add
        end
    "#;
    let foo = parse_module!(&context, "test::foo", foo_source);
    let modules = [foo];

    // create the bundle with locations
    let bundle = Assembler::new(context.source_manager())
        .assemble_library(modules.iter().cloned())
        .unwrap();

    let foo_module_info = bundle.module_infos().next().unwrap();
    assert_eq!(foo_module_info.path(), &PathBuf::new("::test::foo").unwrap());

    let (_, foo_proc) = foo_module_info.procedures().next().unwrap();
    assert_eq!(foo_proc.name, ProcedureName::new("foo").unwrap());

    Ok(())
}

#[test]
fn get_proc_digest_by_name() -> Result<(), Report> {
    let context = TestContext::new();

    let testing_module_source = "
        pub proc foo
            push.1.2 add drop
        end

        pub proc bar
            push.5.6 sub drop
        end
    ";
    let testing_module = parse_module!(&context, "test::names", testing_module_source);

    // create the bundle with locations
    let library = Assembler::new(context.source_manager())
        .assemble_library([testing_module])
        .context("failed to assemble library from testing module")?;

    // get the vector of library procedure digests
    let library_procedure_digests = library
        .exports()
        .filter_map(|export| match export {
            LibraryExport::Procedure(export) => Some(library.mast_forest()[export.node].digest()),
            _ => None,
        })
        .collect::<Vec<Word>>();

    // valid procedure names
    assert!(
        library_procedure_digests.contains(
            &library
                .get_procedure_root_by_path("test::names::foo")
                .expect("procedure with name 'foo' must exist in the test library")
        )
    );
    assert!(
        library_procedure_digests.contains(
            &library
                .get_procedure_root_by_path("test::names::bar")
                .expect("procedure with name 'bar' must exist in the test library")
        )
    );

    // invalid procedure name
    assert_eq!(None, library.get_procedure_root_by_path("test::names::baz"));

    // invalid namespace
    assert_eq!(None, library.get_procedure_root_by_path("invalid::namespace::foo"));

    Ok(())
}

// PROGRAM WITH $main CALL
// ================================================================================================

#[test]
fn simple_main_call() -> TestResult {
    let mut context = TestContext::default();

    // compile account module
    let account_path = PathBuf::new("context::account").unwrap();
    let account_code = context.parse_module_with_path(
        account_path,
        source_file!(
            &context,
            "\
        pub proc account_method_1
            push.2.1 add
        end

        pub proc account_method_2
            push.3.1 sub
        end
        "
        ),
    )?;

    context.add_module(account_code)?;

    // compile note 1 program
    context.assemble(source_file!(
        &context,
        "
        use context::account
        begin
          call.account::account_method_1
        end
        "
    ))?;

    // compile note 2 program
    context.assemble(source_file!(
        &context,
        "
        use context::account
        begin
          call.account::account_method_2
        end
        "
    ))?;
    Ok(())
}

#[test]
fn call_without_path() -> TestResult {
    let mut context = TestContext::default();

    let project =
        miden_project::Package::new("call_without_path", miden_project::Target::executable("main"));

    let account_code1_src = source_file!(
        &context,
        "\
pub proc account_method_1
    push.2.1 add
end

pub proc account_method_2
    push.3.1 sub
end
"
    );
    let account_code2_src = source_file!(
        &context,
        "\
pub proc account_method_1
    push.2.2 add
end

pub proc account_method_2
    push.4.1 sub
end
"
    );

    // compile program in which functions from different modules but with equal names are called
    let main_src = source_file!(
        &context,
        "
        begin
            # call the account_method_1 from the first module (account_code1)
            call.0x81e0b1afdbd431e4c9d4b86599b82c3852ecf507ae318b71c099cdeba0169068

            # call the account_method_2 from the first module (account_code1)
            call.0x1bc375fc794af6637af3f428286bf6ac1a24617640ed29f8bc533f48316c6d75

            # call the account_method_1 from the second module (account_code2)
            call.0xcfadd74886ea075d15826a4f59fb4db3a10cde6e6e953603cba96b4dcbb94321

            # call the account_method_2 from the second module (account_code2)
            call.0x1976bf72d457bd567036d3648b7e3f3c22eca4096936931e59796ec05c0ecb10
        end
        "
    );

    let account_code1 = Module::parse(
        "account_code1",
        ModuleKind::Library,
        account_code1_src,
        context.source_manager(),
    )?;
    let account_code2 = Module::parse(
        "account_code2",
        ModuleKind::Library,
        account_code2_src,
        context.source_manager(),
    )?;
    let main = Module::parse(
        Path::exec_path(),
        ModuleKind::Executable,
        main_src,
        context.source_manager(),
    )?;

    let mut project_assembler = context.project_assembler(project.into())?;
    project_assembler.assemble_with_sources(
        ProjectTargetSelector::Executable("main"),
        "dev",
        ProjectSourceInputs {
            root: main,
            support: vec![account_code1, account_code2],
        },
    )?;

    Ok(())
}

// PROGRAM WITH PROCREF
// ================================================================================================

#[test]
fn procref_call() -> TestResult {
    let mut context = TestContext::default();
    // compile first module
    context.add_module_from_source(
        "module::path::one",
        source_file!(
            &context,
            "
        pub proc aaa
            push.7.8
        end

        pub proc foo
            push.1.2
        end"
        ),
    )?;

    // compile second module
    context.add_module_from_source(
        "module::path::two",
        source_file!(
            &context,
            "
        use module::path::one
        pub use one::foo

        pub proc bar
            procref.one::aaa
        end"
        ),
    )?;

    // compile program with procref calls
    context.assemble(source_file!(
        &context,
        "
        use module::path::two

        @locals(4)
        proc baz
            push.3.4
        end

        begin
            procref.two::bar
            procref.two::foo
            procref.baz
        end"
    ))?;
    Ok(())
}

#[test]
fn get_proc_name_of_unknown_module() -> TestResult {
    let context = TestContext::default();
    // Module `two` is unknown, our error should identify that it is undefined
    let module_source1 = source_file!(
        &context,
        "
    use module::path::two

    pub proc foo
        procref.two::bar
    end"
    );
    let module_path_one = "module::path::one";
    let module1 = context.parse_module_with_path(module_path_one, module_source1)?;

    let report = Assembler::new(context.source_manager())
        .assemble_library(core::iter::once(module1))
        .expect_err("expected unknown module error");

    assert_diagnostic_lines!(
        report,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:2:9\]"#),
        "1 |",
        "2 |     use module::path::two",
        "  :         ^^^^^^^^|^^^^^^^^",
        "  :                 `-- this symbol path could not be resolved",
        "3 |",
        "  `----",
        "help: maybe you are missing an import?"
    );

    Ok(())
}

// CONSTANTS
// ================================================================================================

#[test]
fn simple_constant() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    const TEST_CONSTANT = 7
    begin
        push.TEST_CONSTANT
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn enum_explicit_discriminants() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        r#"
enum Status : u16 {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500,
}

begin
    push.OK
    push.NOT_FOUND
    push.SERVER_ERROR
end
"#
    );
    let _program = context.assemble(source)?;
    Ok(())
}

#[test]
fn enum_discriminants_can_reference_constants() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        r#"
const BASE = 10

enum Status : u16 {
    OK = BASE,
    NOT_FOUND = OK + 1,
}

begin
    push.OK
    push.NOT_FOUND
end
"#
    );
    let _program = context.assemble(source)?;
    Ok(())
}

#[test]
fn enum_felt_repr_variants() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        r#"
enum Status : felt {
    OK = 1,
}

begin
    push.OK
end
"#
    );
    let _program = context.assemble(source)?;
    Ok(())
}

#[test]
fn enum_felt_discriminant_negative_is_rejected() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        r#"
enum Status : felt {
    BAD = 0 - 1,
}

begin
    push.BAD
end
"#
    );
    let err = context
        .assemble(source)
        .expect_err("expected negative discriminant to be rejected");
    assert_diagnostic!(err, "invalid constant expression: value is larger than expected range");
    Ok(())
}

#[test]
fn enum_felt_discriminant_too_large_is_rejected() -> TestResult {
    let context = TestContext::default();
    let modulus = miden_core::Felt::ORDER_U64;
    let source = source_file!(
        &context,
        format!(
            r#"
enum Status : felt {{
    BAD = {modulus},
}}

begin
    push.BAD
end
"#
        )
    );
    let err = context
        .assemble(source)
        .expect_err("expected out-of-range felt discriminant to be rejected");
    assert_diagnostic!(err, "invalid literal: value overflowed the field modulus");
    Ok(())
}

#[test]
fn constant_expression_overflow_is_rejected() -> TestResult {
    let context = TestContext::default();
    let modulus_minus_one = miden_core::Felt::ORDER_U64 - 1;
    let source = source_file!(
        &context,
        format!(
            "const TOO_BIG = {modulus_minus_one} + {modulus_minus_one}\nbegin\n    push.TOO_BIG\nend\n"
        )
    );
    let err = context
        .assemble(source)
        .expect_err("expected constant expression overflow to be rejected");
    assert_diagnostic!(err, "invalid constant expression: value is larger than expected range");
    Ok(())
}

#[test]
fn multiple_constants_push() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const CONSTANT_1 = 21 \
    const CONSTANT_2 = 44 \
    begin \
    push.CONSTANT_1.64.CONSTANT_2.72 \
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn constant_numeric_expression() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT = 11-2+4*(12-(10+1))+9+8//4*2 \
    begin \
    push.TEST_CONSTANT \
    end \
    "
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn constant_alphanumeric_expression() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT_1 = (18-1+10)*6-((13+7)*2) \
    const TEST_CONSTANT_2 = 11-2+4*(12-(10+1))+9
    const TEST_CONSTANT_3 = (TEST_CONSTANT_1-(TEST_CONSTANT_2+10))//5+3
    begin \
    push.TEST_CONSTANT_3 \
    end \
    "
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn constant_hexadecimal_value() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT = 0xFF \
    begin \
    push.TEST_CONSTANT \
    end \
    "
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn constant_field_division() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT = (17//4)/4*(1//2)+2 \
    begin \
    push.TEST_CONSTANT \
    end \
    "
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn constant_err_const_not_initialized() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT = 5+A \
    begin \
    push.TEST_CONSTANT \
    end"
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "undefined constant 'A'",
        regex!(r#",-\[test[\d]+:1:25\]"#),
        "1 | const TEST_CONSTANT = 5+A begin push.TEST_CONSTANT end",
        "  :                         |",
        "  :                         `-- the constant referenced here is not defined in the current scope",
        "  `----",
        " help: are you missing an import?"
    );
    Ok(())
}

#[test]
fn constant_err_div_by_zero() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const TEST_CONSTANT = 5/0 \
    begin \
    push.TEST_CONSTANT \
    end"
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid constant expression: division by zero",
        regex!(r#",-\[test[\d]+:1:23\]"#),
        "1 | const TEST_CONSTANT = 5/0 begin push.TEST_CONSTANT end",
        "  :                       ^^^",
        "  `----"
    );

    let source = source_file!(
        &context,
        "const TEST_CONSTANT = 5//0 \
    begin \
    push.TEST_CONSTANT \
    end"
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid constant expression: division by zero",
        regex!(r#",-\[test[\d]+:1:23\]"#),
        "1 | const TEST_CONSTANT = 5//0 begin push.TEST_CONSTANT end",
        "  :                       ^^^^",
        "  `----"
    );
    Ok(())
}

#[test]
fn constant_err_div_by_zero_indirect() -> TestResult {
    let context = TestContext::default();

    let source = source_file!(
        &context,
        "pub const NUMERATOR = 10
    pub const DENOMINATOR = 0
    pub const BAD_DIV = NUMERATOR / DENOMINATOR

    begin
        push.BAD_DIV
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid constant expression: division by zero",
        regex!(r#",-\[test[\d]+:3:25\]"#),
        "2 |     pub const DENOMINATOR = 0",
        "3 |     pub const BAD_DIV = NUMERATOR / DENOMINATOR",
        "  :                         ^^^^^^^^^^^^^^^^^^^^^^^",
        "4 |",
        "  `----"
    );

    Ok(())
}

#[test]
fn constant_err_div_by_zero_link_time() -> TestResult {
    let mut context = TestContext::default();

    let module_a = source_file!(
        &context,
        "pub const NUMERATOR = 10
        pub const DENOMINATOR = 0"
    );

    context.add_module_from_source("module_a", module_a)?;

    let source = source_file!(
        &context,
        "use module_a::NUMERATOR
    use module_a::DENOMINATOR

    const BAD_DIV = NUMERATOR / DENOMINATOR

    begin
        push.BAD_DIV
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid constant expression: division by zero",
        regex!(r#",-\[test[\d]+:4:21\]"#),
        "3 |",
        "4 |     const BAD_DIV = NUMERATOR / DENOMINATOR",
        "  :                     ^^^^^^^^^^^^^^^^^^^^^^^",
        "5 |",
        "  `----"
    );

    Ok(())
}

#[test]
fn constants_must_be_uppercase() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const constant_1 = 12 \
    begin \
    push.constant_1 \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:7\]"#),
        "1 | const constant_1 = 12 begin push.constant_1 end",
        "  :       ^^^^^|^^^^",
        "  :            `-- found a identifier here",
        "  `----",
        "        help: expected constant identifier"
    );

    Ok(())
}

#[test]
fn duplicate_constant_name() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const CONSTANT = 12 \
    const CONSTANT = 14 \
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "symbol conflict: found duplicate definitions of the same name",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "1 | const CONSTANT = 12 const CONSTANT = 14 begin push.CONSTANT end",
        "  : ^^^^^^^^^|^^^^^^^^^ ^^^^^^^^^|^^^^^^^^^",
        "  :          |                   `-- conflict occurs here",
        "  :          `-- previously defined here",
        "  `----"
    );
    Ok(())
}

#[test]
fn constant_must_be_valid_felt() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "const CONSTANT = 1122INVALID \
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:22\]"#),
        "1 | const CONSTANT = 1122INVALID begin push.CONSTANT end",
        "  :                      ^^^|^^^",
        "  :                         `-- found a constant identifier here",
        "  `----",
        " help: expected \"*\", or \"+\", or \"-\", or \"/\", or \"//\", or \"@\", or \"adv_map\", or \"begin\", or \"const\", or \"enum\", \
or \"proc\", or \"pub\", or \"type\", or \"use\", or end of file, or doc comment"
    );
    Ok(())
}

#[test]
fn constant_must_be_within_valid_felt_range() -> TestResult {
    let context = TestContext::default();

    // test the u64::MAX value
    let source = source_file!(
        &context,
        "const CONSTANT = 18446744073709551615 \
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid literal: value overflowed the field modulus",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | const CONSTANT = 18446744073709551615 begin push.CONSTANT end",
        "  :                  ^^^^^^^^^^^^^^^^^^^^",
        "  `----"
    );

    // test the field modulus value in u64 form
    let source = source_file!(
        &context,
        "const CONSTANT = 18446744069414584321 \
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid literal: value overflowed the field modulus",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | const CONSTANT = 18446744069414584321 begin push.CONSTANT end",
        "  :                  ^^^^^^^^^^^^^^^^^^^^",
        "  `----"
    );

    // test the field modulus value in hex form
    let source = source_file!(
        &context,
        "const CONSTANT = 0xFFFFFFFF00000001 \
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid literal: value overflowed the field modulus",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | const CONSTANT = 0xFFFFFFFF00000001 begin push.CONSTANT end",
        "  :                  ^^^^^^^^^^^^^^^^^^",
        "  `----"
    );

    Ok(())
}

#[test]
fn constants_defined_in_global_scope() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "
    begin \
    const CONSTANT = 12
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:2:11\]"#),
        "1 |",
        "2 |     begin const CONSTANT = 12",
        "  :           ^^|^^",
        "  :             `-- found a const here",
        "3 |     push.CONSTANT end",
        "  `----",
        r#" help: expected primitive opcode (e.g. "add"), or control flow opcode (e.g. "if.true")"#
    );
    Ok(())
}

#[test]
fn constant_not_found() -> TestResult {
    let context = TestContext::new();
    let source = source_file!(
        &context,
        "
    begin \
    push.CONSTANT \
    end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "undefined constant 'CONSTANT'",
        regex!(r#",-\[test[\d]+:2:16\]"#),
        "1 |",
        "2 |     begin push.CONSTANT end",
        "  :                ^^^^|^^^",
        "  :                    `-- the constant referenced here is not defined in the current scope",
        "  `----",
        "help: are you missing an import?"
    );
    Ok(())
}

#[test]
fn mem_operations_with_constants() -> TestResult {
    let context = TestContext::default();

    // Define constant values
    const PROC_LOC_STORE_PTR: u64 = 0;
    const PROC_LOC_LOAD_PTR: u64 = 1;
    const PROC_LOC_STOREW_PTR: u64 = 4;
    const PROC_LOC_LOADW_PTR: u64 = 8;
    const GLOBAL_STORE_PTR: u64 = 12;
    const GLOBAL_LOAD_PTR: u64 = 13;
    const GLOBAL_STOREW_PTR: u64 = 16;
    const GLOBAL_LOADW_PTR: u64 = 20;

    let source = source_file!(
        &context,
        format!(
            "\
    const PROC_LOC_STORE_PTR = {PROC_LOC_STORE_PTR}
    const PROC_LOC_LOAD_PTR = {PROC_LOC_LOAD_PTR}
    const PROC_LOC_STOREW_PTR = {PROC_LOC_STOREW_PTR}
    const PROC_LOC_LOADW_PTR = {PROC_LOC_LOADW_PTR}
    const GLOBAL_STORE_PTR = {GLOBAL_STORE_PTR}
    const GLOBAL_LOAD_PTR = {GLOBAL_LOAD_PTR}
    const GLOBAL_STOREW_PTR = {GLOBAL_STOREW_PTR}
    const GLOBAL_LOADW_PTR = {GLOBAL_LOADW_PTR}

    @locals(12)
    proc test_const_loc
        # constant should resolve using locaddr operation
        locaddr.PROC_LOC_STORE_PTR

        # constant should resolve using loc_store operation
        loc_store.PROC_LOC_STORE_PTR

        # constant should resolve using loc_load operation
        loc_load.PROC_LOC_LOAD_PTR

        # constant should resolve using loc_storew_be operation
        loc_storew_be.PROC_LOC_STOREW_PTR

        # constant should resolve using loc_loadw_be opeartion
        loc_loadw_be.PROC_LOC_LOADW_PTR
    end

    begin
        # inline procedure
        exec.test_const_loc

        # constant should resolve using mem_store operation
        mem_store.GLOBAL_STORE_PTR

        # constant should resolve using mem_load operation
        mem_load.GLOBAL_LOAD_PTR

        # constant should resolve using mem_storew_be operation
        mem_storew_be.GLOBAL_STOREW_PTR

        # constant should resolve using mem_loadw_be operation
        mem_loadw_be.GLOBAL_LOADW_PTR
    end
    "
        )
    );
    let program = context.assemble(source)?;

    // Define expected
    let expected = source_file!(
        &context,
        format!(
            "\
    @locals(12)
    proc test_const_loc
        # constant should resolve using locaddr operation
        locaddr.{PROC_LOC_STORE_PTR}

        # constant should resolve using loc_store operation
        loc_store.{PROC_LOC_STORE_PTR}

        # constant should resolve using loc_load operation
        loc_load.{PROC_LOC_LOAD_PTR}

        # constant should resolve using loc_storew_be operation
        loc_storew_be.{PROC_LOC_STOREW_PTR}

        # constant should resolve using loc_loadw_be opeartion
        loc_loadw_be.{PROC_LOC_LOADW_PTR}
    end

    begin
        # inline procedure
        exec.test_const_loc

        # constant should resolve using mem_store operation
        mem_store.{GLOBAL_STORE_PTR}

        # constant should resolve using mem_load operation
        mem_load.{GLOBAL_LOAD_PTR}

        # constant should resolve using mem_storew_be operation
        mem_storew_be.{GLOBAL_STOREW_PTR}

        # constant should resolve using mem_loadw_be operation
        mem_loadw_be.{GLOBAL_LOADW_PTR}
    end
    "
        )
    );
    let expected_program = context.assemble(expected)?;
    assert_eq!(expected_program.to_string(), program.to_string());
    Ok(())
}

#[test]
fn const_conversion_failed_to_u16() -> TestResult {
    // Define constant value greater than u16::MAX
    let constant_value: u64 = u16::MAX as u64 + 1;

    let context = TestContext::default();
    let source = source_file!(
        &context,
        format!(
            "\
    const CONSTANT = {constant_value}

    @locals(1)
    proc test_constant_overflow
        loc_load.CONSTANT
    end

    begin
        exec.test_constant_overflow
    end
    "
        )
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid immediate: value is larger than expected range",
        regex!(r#",-\[test[\d]+:5:18\]"#),
        "4 |     proc test_constant_overflow",
        "5 |         loc_load.CONSTANT",
        "  :                  ^^^^^^^^",
        "6 |     end",
        "  `----"
    );
    Ok(())
}

#[test]
fn const_conversion_failed_to_u32() -> TestResult {
    let context = TestContext::default();
    // Define constant value greater than u16::MAX
    let constant_value: u64 = u32::MAX as u64 + 1;

    let source = source_file!(
        &context,
        format!(
            "\
    const CONSTANT = {constant_value}

    begin
        mem_load.CONSTANT
    end
    "
        )
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid immediate: value is larger than expected range",
        regex!(r#",-\[test[\d]+:4:18\]"#),
        "3 |     begin",
        "4 |         mem_load.CONSTANT",
        "  :                  ^^^^^^^^",
        "5 |     end",
        "  `----"
    );
    Ok(())
}

#[test]
fn deprecated_mem_loadw_instruction() -> TestResult {
    let context = TestContext::default();

    let source = source_file!(
        &context,
        "\
    begin
        mem_loadw
    end
    "
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "deprecated instruction: `mem_loadw` has been removed",
        regex!(r#",-\[test[\d]+:2:9\]"#),
        "1 | begin",
        "2 |         mem_loadw",
        regex!(r#"^ *: *\^+"#),
        regex!(r#"this instruction is no longer supported"#),
        "3 |     end",
        "  `----",
        regex!(r#"help:.*use.*mem_loadw_be.*instead"#)
    );
    Ok(())
}

#[test]
fn deprecated_loc_loadw_instruction() -> TestResult {
    let context = TestContext::default();

    let source = source_file!(
        &context,
        "\
    @locals(8)
    proc foo
        loc_loadw.0
    end
    begin
        exec.foo
    end
    "
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "deprecated instruction: `loc_loadw` has been removed",
        regex!(r#",-\[test[\d]+:3:9\]"#),
        "2 |     proc foo",
        "3 |         loc_loadw.0",
        regex!(r#"^ *: *\^+"#),
        regex!(r#"this instruction is no longer supported"#),
        "4 |     end",
        "  `----",
        regex!(r#"help:.*use.*loc_loadw_be.*instead"#)
    );
    Ok(())
}

#[test]
fn deprecated_loc_storew_instruction() -> TestResult {
    let context = TestContext::default();

    let source = source_file!(
        &context,
        "\
    @locals(8)
    proc foo
        loc_storew.0
    end
    begin
        exec.foo
    end
    "
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "deprecated instruction: `loc_storew` has been removed",
        regex!(r#",-\[test[\d]+:3:9\]"#),
        "2 |     proc foo",
        "3 |         loc_storew.0",
        regex!(r#"^ *: *\^+"#),
        regex!(r#"this instruction is no longer supported"#),
        "4 |     end",
        "  `----",
        regex!(r#"help:.*use.*loc_storew_be.*instead"#)
    );
    Ok(())
}

#[test]
fn const_word_from_string() -> TestResult {
    let context = TestContext::default();
    let sample_source_string = "lorem ipsum";

    let source = source_file!(
        &context,
        format!(
            r#"
    const SAMPLE_WORD = word("{sample_source_string}")

    begin
        push.SAMPLE_WORD
    end
    "#
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);

    Ok(())
}

/// Check that the event ID conversion during compilation is consistent with
/// string_to_event_id.
#[test]
fn const_event_from_string() -> TestResult {
    let context = TestContext::default();
    let sample_event_name = "miden::test::constant";
    let expected_felt = EventId::from_name(sample_event_name);

    let source1 = source_file!(
        &context,
        format!(
            r#"
    begin
        emit.event("{sample_event_name}")
    end
    "#
        )
    );
    let source2 = source_file!(
        &context,
        format!(
            r#"
    begin
        push.{expected_felt}
        emit
        drop
    end
    "#
        )
    );

    let program1 = context.assemble(source1)?;
    let program2 = context.assemble(source2)?;
    assert_eq!(program1.hash(), program2.hash());

    Ok(())
}

#[test]
fn test_push_word_slice() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    const SAMPLE_WORD = [2, 3, 4, 5]
    const SAMPLE_HEX_WORD = 0x0600000000000000070000000000000008000000000000000900000000000000

    begin
        push.SAMPLE_WORD[1..3]
        push.SAMPLE_WORD[0]
        push.[10, 11, 12, 13][1..3]

        push.SAMPLE_HEX_WORD[2..4]
        push.0x0600000000000000070000000000000008000000000000000900000000000000[0..2]
    end
    "
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn test_push_word_slice_invalid() -> TestResult {
    let context = TestContext::default();
    let source_invalid_range = source_file!(
        &context,
        format!(
            "\
    const SAMPLE_WORD = [2, 3, 4, 5]

    begin
        push.SAMPLE_WORD[6..3]
    end
    "
        )
    );
    assert!(context.assemble(source_invalid_range).is_err());

    let source_empty_range = source_file!(
        &context,
        format!(
            "\
    const SAMPLE_WORD = [2, 3, 4, 5]

    begin
        push.SAMPLE_WORD[2..2]
    end
    "
        )
    );
    assert!(context.assemble(source_empty_range).is_err());

    let source_invalid_constant_type = source_file!(
        &context,
        format!(
            "\
    const SAMPLE_VALUE = 6
    begin
        push.SAMPLE_VALUE[1..3]
    end
    "
        )
    );
    assert!(context.assemble(source_invalid_constant_type).is_err());

    let source_invalid_constant_type = source_file!(
        &context,
        format!(
            "\
    begin
        push.5[0..2]
    end
    "
        )
    );
    assert!(context.assemble(source_invalid_constant_type).is_err());

    Ok(())
}

#[test]
fn link_time_const_evaluation_succeeds() -> TestResult {
    let context = TestContext::default();
    let a = r#"
            pub const FOO = 1
            pub proc f
                push.FOO
            end
        "#;
    let a = parse_module!(&context, "lib::a", a);

    let lib = Assembler::new(context.source_manager()).assemble_library([a])?;

    let program_source = source_file!(
        &context,
        "\
        use lib::a
        use a::FOO
        begin
            push.FOO
            exec.a::f
            add
            add
        end"
    );

    let program = Assembler::new(context.source_manager())
        .with_dynamic_library(lib)?
        .assemble_program(program_source)?;
    insta::assert_snapshot!(program);

    Ok(())
}

#[test]
fn link_time_const_evaluation_undefined_symbol() -> TestResult {
    let context = TestContext::default();
    let a = r#"
            pub proc f
                push.1
            end
        "#;
    let a = parse_module!(&context, "lib::a", a);

    let lib = Assembler::new(context.source_manager()).assemble_library([a])?;

    let source = source_file!(
        &context,
        "\
        use lib::a::FOO
        begin
            push.FOO
            exec.lib::a::f
            add
        end"
    );

    let error = Assembler::new(context.source_manager())
        .with_dynamic_library(lib)?
        .assemble_program(source)
        .expect_err("expected diagnostic to be raised, but compilation succeeded");
    assert_diagnostic_lines!(
        error,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:1:5\]"#),
        "1 | use lib::a::FOO",
        "  :     ^^^^^|^^^^^",
        "  :          `-- this symbol path could not be resolved",
        "2 |         begin",
        "  `----",
        "help: maybe you are missing an import?"
    );

    Ok(())
}

#[test]
fn link_time_const_evaluation_invalid_constant() -> TestResult {
    let context = TestContext::default();
    let a = r#"
            pub proc f
                push.1
            end
        "#;
    let a = parse_module!(&context, "lib::a", a);

    let lib = Assembler::new(context.source_manager()).assemble_library([a])?;

    let source = source_file!(
        &context,
        "\
    use lib::a::f
    begin
        push.f
    end"
    );

    let error = Assembler::new(context.source_manager())
        .with_dynamic_library(lib)?
        .assemble_program(source)
        .expect_err("expected diagnostic to be raised, but compilation succeeded");

    assert_diagnostic_lines!(
        error,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:3:14\]"#),
        "2 |     begin",
        "3 |         push.f",
        "  :              |",
        "  :              `-- found a identifier here",
        "4 |     end",
        "  `----",
        "help: expected \"[\", or constant identifier, or hex-encoded literal, or hex_word, or integer literal"
    );

    Ok(())
}
// DECORATORS
// ================================================================================================

#[test]
fn decorators_basic_block() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0
        add
        trace.1
        mul
        trace.2
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn decorators_repeat_one_basic_block() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0
        repeat.2 add end
        trace.1
        repeat.2 mul end
        trace.2
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn decorators_repeat_split() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0
        repeat.2
            if.true
                trace.1 push.42 trace.2
            else
                trace.3 push.22 trace.3
            end
            trace.4
        end
        trace.5
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn decorators_call() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0 trace.1
        call.0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
        trace.2
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn decorators_dyn() -> TestResult {
    // single line
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0
        dynexec
        trace.1
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    // multi line
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0 trace.1 trace.2 trace.3 trace.4
        dynexec
        trace.5 trace.6 trace.7 trace.8 trace.9
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn decorators_external() -> TestResult {
    let context = TestContext::default();
    let baz = r#"
        pub proc f
            push.7 push.8 sub
        end
    "#;
    let baz = parse_module!(&context, "lib::baz", baz);

    let lib = Assembler::new(context.source_manager()).assemble_library([baz])?;

    let program_source = source_file!(
        &context,
        "\
    use lib::baz
    begin
        trace.0
        exec.baz::f
        trace.1
    end"
    );

    let program = Assembler::new(context.source_manager())
        .with_dynamic_library(lib)?
        .assemble_program(program_source)?;
    insta::assert_snapshot!(program);

    Ok(())
}

#[test]
fn decorators_join_and_split() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    begin
        trace.0 trace.1
        if.true
            trace.2 add trace.3
        else
            trace.4 mul trace.5
        end
        trace.6
        if.true
            trace.7 push.42 trace.8
        else
            trace.9 push.22 trace.10
        end
        trace.11
    end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

// ASSERTIONS
// ================================================================================================

#[test]
fn assert_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        assert
        assert.err=ERR1
        assert.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn assertz_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        assertz
        assertz.err=ERR1
        assertz.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn assert_eq_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        assert_eq
        assert_eq.err=ERR1
        assert_eq.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn assert_eqw_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        assert_eqw
        assert_eqw.err=ERR1
        assert_eqw.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn u32assert_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        u32assert
        u32assert.err=ERR1
        u32assert.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn u32assert2_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        u32assert2
        u32assert2.err=ERR1
        u32assert2.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn u32assertw_with_code() -> TestResult {
    let context = TestContext::default();
    let err_msg = "Oh no";
    let source = source_file!(
        &context,
        format!(
            "\
    const ERR1 = \"{err_msg}\"

    begin
        u32assertw
        u32assertw.err=ERR1
        u32assertw.err=\"{err_msg}\"
    end
    "
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

/// Ensure that there is no collision between `Assert`, `U32assert2`, and `MpVerify`
/// instructions with different inner values (which all don't contribute to the MAST root).
#[test]
fn asserts_and_mpverify_with_code_in_duplicate_procedure() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    proc f1
        u32assert.err=\"1\"
    end
    proc f2
        u32assert.err=\"2\"
    end
    proc f12
        u32assert.err=\"1\"
        u32assert.err=\"2\"
    end
    proc f21
        u32assert.err=\"2\"
        u32assert.err=\"1\"
    end
    proc g1
        assert.err=\"1\"
    end
    proc g2
        assert.err=\"2\"
    end
    proc g12
        assert.err=\"1\"
        assert.err=\"2\"
    end
    proc g21
        assert.err=\"2\"
        assert.err=\"1\"
    end
    proc fg
        assert.err=\"1\"
        u32assert.err=\"1\"
        assert.err=\"2\"
        u32assert.err=\"2\"

        u32assert.err=\"1\"
        assert.err=\"1\"
        u32assert.err=\"2\"
        assert.err=\"2\"
    end

    proc mpverify
        mtree_verify.err=\"1\"
        mtree_verify.err=\"2\"
        mtree_verify.err=\"2\"
        mtree_verify.err=\"1\"
    end

    begin
        exec.f1
        exec.f2
        exec.f12
        exec.f21
        exec.g1
        exec.g2
        exec.g12
        exec.g21
        exec.fg
        exec.mpverify
    end
    "
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn mtree_verify_with_code() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
    const ERR1 = \"1\"

    begin
        mtree_verify
        mtree_verify.err=ERR1
        mtree_verify.err=\"2\"
    end
    "
    );

    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

// NESTED CONTROL BLOCKS
// ================================================================================================

#[test]
fn nested_control_blocks() -> TestResult {
    let context = TestContext::default();

    // if with else
    let source = source_file!(
        &context,
        "begin \
        push.2 push.3 \
        if.true \
            add while.true push.7 push.11 add end \
        else \
            mul repeat.2 push.8 end if.true mul end  \
        end
        push.3 add
        end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

fn nested_if_source(depth: usize) -> String {
    let mut source = String::from("begin\n");
    for _ in 0..depth {
        source.push_str("push.1\nif.true\n");
    }
    source.push_str("push.1\n");
    for _ in 0..depth {
        source.push_str("end\n");
    }
    source.push_str("end\n");
    source
}

#[test]
fn control_flow_nesting_depth_boundary() -> TestResult {
    let context = TestContext::default();
    let source = nested_if_source(MAX_CONTROL_FLOW_NESTING);
    let source = source_file!(&context, source.as_str());
    context.assemble(source)?;
    Ok(())
}

#[test]
fn control_flow_nesting_depth_exceeded() -> TestResult {
    let context = TestContext::default();
    let source = nested_if_source(MAX_CONTROL_FLOW_NESTING + 1);
    let source = source_file!(&context, source.as_str());
    let error = context
        .assemble(source)
        .expect_err("expected diagnostic to be raised, but compilation succeeded");
    assert_diagnostic!(&error, "control-flow nesting depth exceeded");
    Ok(())
}

// PROGRAMS WITH PROCEDURES
// ================================================================================================

/// If the program has 2 procedures with the same MAST root (but possibly different decorators), the
/// correct procedure is chosen on exec
#[test]
fn ensure_correct_procedure_selection_on_collision() -> TestResult {
    let context = TestContext::default();

    // if with else
    let source = source_file!(
        &context,
        "
        proc f
            add
        end

        proc g
            trace.2
            add
        end

        begin
            if.true
                exec.f
            else
                exec.g
            end
        end"
    );
    let program = context.assemble(source)?;

    // Note: those values were taken from adding prints to the assembler at the time of writing. It
    // is possible that this test starts failing if we end up ordering procedures differently.
    let expected_f_node_id =
        MastNodeId::from_u32_safe(1_u32, program.mast_forest().as_ref()).unwrap();
    let expected_g_node_id =
        MastNodeId::from_u32_safe(0_u32, program.mast_forest().as_ref()).unwrap();

    let (exec_f_node_id, exec_g_node_id) = {
        let split_node_id = {
            // Note: the program starts with a join node, which joins:
            // - left: the fmp initialization sequence,
            // - right: the actual entrypoint of the program (the if statement).
            let root_join_id = program.entrypoint();
            program.mast_forest()[root_join_id].unwrap_join().second()
        };
        let split_node = &program.mast_forest()[split_node_id].unwrap_split();

        (split_node.on_true(), split_node.on_false())
    };

    assert_eq!(program.mast_forest()[expected_f_node_id], program.mast_forest()[exec_f_node_id]);
    assert_eq!(program.mast_forest()[expected_g_node_id], program.mast_forest()[exec_g_node_id]);

    Ok(())
}

#[test]
fn program_with_one_procedure() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_nested_procedure() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
        proc foo push.3 push.7 mul end \
        proc bar push.5 exec.foo add end \
        begin push.2 push.4 add exec.foo push.11 exec.bar sub end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_proc_locals() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
        @locals(4) proc foo \
            loc_store.0 \
            add \
            loc_load.0 \
            mul \
        end \
        begin \
            push.10 push.9 push.8 \
            exec.foo \
        end"
    );
    let program = context.assemble(source)?;
    // Note: 18446744069414584317 == -4 (mod 2^64 - 2^32 + 1)
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_proc_locals_fail() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
proc foo
    loc_store.0
    add
    loc_load.0
    mul
end
begin
    push.4 push.3 push.2
    exec.foo
end"
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid procedure local reference",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "1 | ,-> proc foo",
        "2 | |       loc_store.0",
        "  : |       ^^^^^|^^^^^",
        "  : |            `-- the procedure local index referenced here is invalid",
        "3 | |       add",
        "4 | |       loc_load.0",
        "5 | |       mul",
        "6 | |-> end",
        "  : `---- this procedure definition does not allocate any locals",
        "7 |     begin",
        "  `----"
    );

    Ok(())
}

#[test]
fn program_with_exported_procedure() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "pub proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid program: procedure exports are not allowed",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "1 | pub proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end",
        "  : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
        "  `----",
        "        help: perhaps you meant to use `proc` instead of `export`?"
    );
    Ok(())
}

// PROGRAMS WITH DYNAMIC CODE BLOCKS
// ================================================================================================

#[test]
fn program_with_dynamic_code_execution() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin dynexec end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_dynamic_code_execution_in_new_context() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin dyncall end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

// MAST ROOT CALLS
// ================================================================================================

#[test]
fn program_with_incorrect_mast_root_length() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin call.0x1234 end");

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid MAST root literal",
        regex!(r#",-\[test[\d]+:1:12\]"#),
        "1 | begin call.0x1234 end",
        "  :            ^^^^^^",
        "  `----"
    );
    Ok(())
}

#[test]
fn program_with_invalid_mast_root_chars() {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a21xyzb end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid literal: expected 2, 4, 8, 16, or 64 hex digits",
        regex!(r#",-\[test[\d]+:1:12\]"#),
        "1 | begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a21xyzb end",
        "  :            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
        "  `----"
    );
}

#[test]
fn program_with_invalid_rpo_digest_call() {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "begin call.0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "invalid literal: value overflowed the field modulus",
        regex!(r#",-\[test[\d]+:1:12\]"#),
        "1 | begin call.0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff end",
        "  :            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
        "  `----"
    );
}

#[test]
fn program_with_phantom_mast_call() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a213dae end"
    );
    let ast = context.parse_program(source)?;

    let assembler = Assembler::new(context.source_manager());
    assembler.assemble_program(ast)?;
    Ok(())
}

// IMPORTS
// ================================================================================================

#[test]
fn program_with_one_import_and_hex_call() -> TestResult {
    const MODULE: &str = "dummy::math::u256";
    const PROCEDURE: &str = r#"
        pub proc iszero_unsafe
            eq.0
            repeat.7
                swap
                eq.0
                and
            end
        end"#;

    let mut context = TestContext::default();
    let path = MODULE;
    let ast =
        context.parse_module_with_path(path, source_file!(&context, PROCEDURE.to_string()))?;
    let library = Assembler::new(context.source_manager())
        .assemble_library(core::iter::once(ast))
        .unwrap();

    context.add_library(&library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {MODULE}
        begin
            push.4 push.3
            exec.u256::iszero_unsafe
            call.0x20234ee941e53a15886e733cc8e041198c6e90d2a16ea18ce1030e8c3596dd38
        end"#
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_two_imported_procs_with_same_mast_root() -> TestResult {
    const MODULE: &str = "dummy::math::u256";
    const PROCEDURE: &str = r#"
        pub proc iszero_unsafe_dup
            eq.0
            repeat.7
                swap
                eq.0
                and
            end
        end

        pub proc iszero_unsafe
            eq.0
            repeat.7
                swap
                eq.0
                and
            end
        end"#;

    let mut context = TestContext::default();
    let path = MODULE;
    let ast =
        context.parse_module_with_path(path, source_file!(&context, PROCEDURE.to_string()))?;
    let library = Assembler::new(context.source_manager())
        .assemble_library(core::iter::once(ast))
        .unwrap();

    context.add_library(&library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {MODULE}
        begin
            push.4 push.3
            exec.u256::iszero_unsafe
            exec.u256::iszero_unsafe_dup
        end"#
        )
    );
    context.assemble(source)?;
    Ok(())
}

#[test]
fn program_with_reexported_proc_in_same_library() -> TestResult {
    // exprted proc is in same library
    const REF_MODULE: &str = "dummy1::math::u64";
    const REF_MODULE_BODY: &str = r#"
        pub proc checked_eqz
            u32assert2
            eq.0
            swap
            eq.0
            and
        end
        pub proc unchecked_eqz
            eq.0
            swap
            eq.0
            and
        end
    "#;

    const MODULE: &str = "dummy1::math::u256";
    const MODULE_BODY: &str = r#"
        use dummy1::math::u64

        #! checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise
        pub use u64::checked_eqz # re-export

        #! unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise
        pub use u64::unchecked_eqz->notchecked_eqz # re-export with alias
    "#;

    let mut context = TestContext::new();
    let mut parser = Module::parser(ModuleKind::Library);
    let ast = parser.parse_str(MODULE, MODULE_BODY, context.source_manager()).unwrap();

    // check docs
    let docs_checked_eqz = ast
        .aliases()
        .find(|p| p.name().as_str() == "checked_eqz")
        .unwrap()
        .docs()
        .unwrap();
    assert_eq!(
        docs_checked_eqz,
        "checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise\n"
    );
    let docs_unchecked_eqz = ast
        .aliases()
        .find(|p| p.name().as_str() == "notchecked_eqz")
        .unwrap()
        .docs()
        .unwrap();
    assert_eq!(
        docs_unchecked_eqz,
        "unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise\n"
    );

    let mut parser = Module::parser(ModuleKind::Library);
    let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, context.source_manager()).unwrap();

    let library = Assembler::new(context.source_manager())
        .assemble_library([ast, ref_ast])
        .unwrap();

    context.add_library(&library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {MODULE}
        begin
            push.4 push.3
            exec.u256::checked_eqz
            exec.u256::notchecked_eqz
        end"#
        )
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_reexported_custom_alias_in_same_library() -> TestResult {
    // exprted proc is in same library
    const REF_MODULE: &str = "dummy1::math::u64";
    const REF_MODULE_BODY: &str = r#"
        pub proc checked_eqz
            u32assert2
            eq.0
            swap
            eq.0
            and
        end
        pub proc unchecked_eqz
            eq.0
            swap
            eq.0
            and
        end
    "#;

    const MODULE: &str = "dummy1::math::u256";
    const MODULE_BODY: &str = r#"
        use dummy1::math::u64->myu64

        #! checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise
        pub use myu64::checked_eqz # re-export

        #! unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise
        pub use myu64::unchecked_eqz->notchecked_eqz # re-export with alias
    "#;

    let mut context = TestContext::new();
    let mut parser = Module::parser(ModuleKind::Library);
    let ast = parser.parse_str(MODULE, MODULE_BODY, context.source_manager()).unwrap();

    let mut parser = Module::parser(ModuleKind::Library);
    let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, context.source_manager()).unwrap();

    let library = Assembler::new(context.source_manager())
        .assemble_library([ast, ref_ast])
        .unwrap();

    context.add_library(&library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {MODULE}->myu256
        begin
            push.4 push.3
            exec.myu256::checked_eqz
            exec.myu256::notchecked_eqz
        end"#
        )
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn program_with_reexported_proc_in_another_library() -> TestResult {
    // when re-exported proc is part of a different library
    const REF_MODULE: &str = "dummy2::math::u64";
    const REF_MODULE_BODY: &str = r#"
        pub proc checked_eqz
            u32assert2
            eq.0
            swap
            eq.0
            and
        end
        pub proc unchecked_eqz
            eq.0
            swap
            eq.0
            and
        end
    "#;

    const MODULE: &str = "dummy1::math::u256";
    const MODULE_BODY: &str = r#"
        use dummy2::math::u64
        pub use u64::checked_eqz # re-export
        pub use u64::unchecked_eqz->notchecked_eqz # re-export with alias
    "#;

    let mut context = TestContext::default();
    let mut parser = Module::parser(ModuleKind::Library);
    let source_manager = context.source_manager();
    // We reference code in this module
    let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, source_manager.clone())?;
    // But only exports from this module are exposed by the library
    let ast = parser.parse_str(MODULE, MODULE_BODY, source_manager.clone())?;

    let dummy_library = {
        let mut assembler = Assembler::new(source_manager);
        assembler.compile_and_statically_link(ref_ast)?;
        assembler.assemble_library([ast])?
    };

    // Now we want to use the the library we've compiled
    context.add_library(&dummy_library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {MODULE}
        begin
            push.4 push.3
            exec.u256::checked_eqz
            exec.u256::notchecked_eqz
        end"#
        )
    );
    let program = context.assemble(source)?;

    insta::assert_snapshot!(program);

    // We also want to assert that exports from the referenced module do not leak
    let mut context = TestContext::default();
    context.add_library(dummy_library)?;

    let source = source_file!(
        &context,
        format!(
            r#"
        use {REF_MODULE}
        begin
            push.4 push.3
            exec.u64::checked_eqz
            exec.u64::notchecked_eqz
        end"#
        )
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:2:13\]"#),
        "1 |",
        "2 |         use dummy2::math::u64",
        "  :             ^^^^^^^^|^^^^^^^^",
        "  :                     `-- this symbol path could not be resolved",
        "3 |         begin",
        "  `----",
        "help: maybe you are missing an import?"
    );
    Ok(())
}

#[test]
fn module_alias() -> TestResult {
    const MODULE: &str = "dummy::math::u64";
    const PROCEDURE: &str = r#"
        pub proc checked_add
            swap
            movup.3
            u32assert2
            u32widening_add
            movup.3
            movup.3
            u32assert2
            u32widening_add3
            eq.0
            assert
        end"#;

    let mut context = TestContext::default();
    let source_manager = context.source_manager();
    let mut parser = Module::parser(ModuleKind::Library);
    let ast = parser.parse_str(MODULE, PROCEDURE, source_manager.clone()).unwrap();
    let library = Assembler::new(source_manager).assemble_library([ast]).unwrap();

    context.add_library(&library)?;

    let source = source_file!(
        &context,
        "
        use dummy::math::u64->bigint

        begin
            push.1.0
            push.2.0
            exec.bigint::checked_add
        end"
    );

    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);

    // --- invalid module alias -----------------------------------------------
    let source = source_file!(
        &context,
        "
        use dummy::math::u64->bigint->invalidname

        begin
            push.1.0
            push.2.0
            exec.bigint->invalidname::checked_add
        end"
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:2:37\]"#),
        "1 |",
        "2 |         use dummy::math::u64->bigint->invalidname",
        "  :                                     ^|",
        "  :                                      `-- found a -> here",
        "3 |",
        "  `----",
        r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or end of file, or doc comment"#
    );

    Ok(())
}

#[test]
//#[ignore = "disabled until unused import accuracy is improved"]
fn module_alias_unused_import() -> TestResult {
    const MODULE: &str = "dummy::math::u64";
    const PROCEDURE: &str = r#"
        pub proc checked_add
            swap
            movup.3
            u32assert2
            u32widening_add
            movup.3
            movup.3
            u32assert2
            u32widening_add3
            eq.0
            assert
        end"#;

    let mut context = TestContext::default();
    let source_manager = context.source_manager();
    let mut parser = Module::parser(ModuleKind::Library);
    let ast = parser.parse_str(MODULE, PROCEDURE, source_manager.clone()).unwrap();
    let library = Assembler::new(source_manager).assemble_library([ast]).unwrap();

    context.add_library(&library)?;

    // --- duplicate module import --------------------------------------------
    let source = source_file!(
        &context,
        "
        use dummy::math::u64
        use dummy::math::u64->bigint

        begin
            push.1.0
            push.2.0
            exec.bigint::checked_add
        end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "unused import",
        regex!(r#",-\[test[\d]+:2:13\]"#),
        "1 |",
        "2 |         use dummy::math::u64",
        "  :             ^^^^^^^^^^^^^^^^",
        "3 |         use dummy::math::u64->bigint",
        "  `----",
        " help: this import is never used and can be safely removed"
    );

    // --- duplicate module imports with different aliases --------------------
    // TODO: Do we actually want this to be a warning/error? If the imports
    // have different aliases, there might be some use for that when refactoring
    // code or something. Anyway, I'm disabling the test that expects this to
    // fail for the time being
    /*
    let source = source_file!(
    &context,
        "
        use dummy::math::u64->bigint
        use dummy::math::u64->bigint2

        begin
            push.1.0
            push.2.0
            exec.bigint::checked_add
            exec.bigint2::checked_add
        end"
    );
    */
    Ok(())
}

#[test]
fn program_with_import_errors() {
    let context = TestContext::default();
    // --- non-existent import ------------------------------------------------
    let source = source_file!(
        &context,
        "\
        use miden::core::math::u512
        begin \
            push.4 push.3 \
            exec.u512::iszero_unsafe \
        end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:1:5\]"#),
        "1 | use miden::core::math::u512",
        "  :     ^^^^^^^^^^^|^^^^^^^^^^^",
        "  :                `-- this symbol path could not be resolved",
        "2 |         begin push.4 push.3 exec.u512::iszero_unsafe end",
        "  `----",
        "help: maybe you are missing an import?"
    );

    // --- non-existent procedure in import -----------------------------------
    let source = source_file!(
        &context,
        "\
        use miden::core::math::u256
        begin \
            push.4 push.3 \
            exec.u256::foo \
        end"
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:1:5\]"#),
        "1 | use miden::core::math::u256",
        "  :     ^^^^^^^^^^^|^^^^^^^^^^^",
        "  :                `-- this symbol path could not be resolved",
        "2 |         begin push.4 push.3 exec.u256::foo end",
        "  `----",
        "help: maybe you are missing an import?"
    );
}

// COMMENTS
// ================================================================================================

#[test]
fn comment_simple() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin # simple comment \n push.1 push.2 add end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn comment_in_nested_control_blocks() -> TestResult {
    let context = TestContext::default();

    // if with else
    let source = source_file!(
        &context,
        "begin \
        push.1 push.2 \
        if.true \
            # nested comment \n\
            add while.true push.7 push.11 add end \
        else \
            mul repeat.2 push.8 end if.true mul end  \
            # nested comment \n\
        end
        push.3 add
        end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn comment_before_program() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, " # starting comment \n begin push.1 push.2 add end");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn comment_after_program() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin push.1 push.2 add end # closing comment");
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn can_push_constant_word() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
const A = 0x0200000000000000030000000000000004000000000000000500000000000000
begin
    push.A
end"
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn test_advmap_push() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
adv_map A(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01]
begin push.A adv.push_mapval assert end"
    );

    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn test_advmap_push_nokey() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
adv_map A = [0x01]
begin push.A adv.push_mapval assert end"
    );

    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

#[test]
fn test_adv_has_map_key() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
adv_map A(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01]
begin adv.has_mapkey assert end"
    );

    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

// ERRORS
// ================================================================================================

#[test]
fn invalid_empty_program() {
    let context = TestContext::default();
    assert_assembler_diagnostic!(
        context,
        source_file!(&context, ""),
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "`----",
        r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
    );

    assert_assembler_diagnostic!(
        context,
        source_file!(&context, ""),
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "  `----",
        r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
    );
}

#[test]
fn invalid_program_unrecognized_token() {
    let context = TestContext::default();
    assert_assembler_diagnostic!(
        context,
        source_file!(&context, "none"),
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:1\]"#),
        "1 | none",
        "  : ^^|^",
        "  :   `-- found a identifier here",
        "  `----",
        r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
    );
}

#[test]
fn invalid_program_unmatched_begin() {
    let context = TestContext::default();
    assert_assembler_diagnostic!(
        context,
        source_file!(&context, "begin add"),
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:10\]"#),
        "1 | begin add",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );
}

#[test]
fn invalid_program_invalid_top_level_token() {
    let context = TestContext::default();
    assert_assembler_diagnostic!(
        context,
        source_file!(&context, "begin add end mul"),
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:15\]"#),
        "1 | begin add end mul",
        "  :               ^|^",
        "  :                `-- found a mul here",
        "  `----",
        r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or end of file, or doc comment"#
    );
}

#[test]
fn invalid_proc_missing_end_unexpected_begin() {
    let context = TestContext::default();
    let source = source_file!(&context, "proc foo add mul begin push.1 end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | proc foo add mul begin push.1 end",
        "  :                  ^^|^^",
        "  :                    `-- found a begin here",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );
}

#[test]
fn invalid_proc_missing_end_unexpected_proc() {
    let context = TestContext::default();
    let source = source_file!(&context, "proc foo add mul proc bar push.3 end begin push.1 end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | proc foo add mul proc bar push.3 end begin push.1 end",
        "  :                  ^^|^",
        "  :                    `-- found a proc here",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );
}

#[test]
fn invalid_proc_undefined_local() {
    let context = TestContext::default();
    let source = source_file!(&context, "proc foo add mul end begin push.1 exec.bar end");
    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:1:40\]"#),
        "1 | proc foo add mul end begin push.1 exec.bar end",
        "  :                                        ^|^",
        "  :                                         `-- this symbol path could not be resolved",
        "  `----",
        " help: maybe you are missing an import?"
    );
}

#[test]
fn missing_import() {
    let context = TestContext::new();
    let source = source_file!(
        &context,
        r#"
    begin
        exec.u64::add
    end"#
    );

    assert_assembler_diagnostic!(
        context,
        source,
        "undefined symbol reference",
        regex!(r#",-\[test[\d]+:3:14\]"#),
        "2 |     begin",
        "3 |         exec.u64::add",
        "  :              ^^^^|^^^",
        "  :                  `-- this symbol path could not be resolved",
        "4 |     end",
        "  `----",
        "help: maybe you are missing an import?"
    );
}

#[test]
fn invalid_proc_invalid_numeric_name() {
    let context = TestContext::default();
    let source = source_file!(&context, "proc 123 add mul end begin push.1 exec.123 end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:6\]"#),
        "1 | proc 123 add mul end begin push.1 exec.123 end",
        "  :      ^|^",
        "  :       `-- found a integer here",
        "  `----",
        " help: expected primitive opcode",
        "      identifier"
    );
}

#[test]
fn invalid_proc_duplicate_procedure_name() {
    let context = TestContext::default();
    let source =
        source_file!(&context, "proc foo add mul end proc foo push.3 end begin push.1 end");
    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "symbol conflict: found duplicate definitions of the same name",
        regex!(r#",-\[test[\d]+:1:6\]"#),
        "1 | proc foo add mul end proc foo push.3 end begin push.1 end",
        "  :      ^|^             ^^^^^^^^^|^^^^^^^^^",
        "  :       |                       `-- conflict occurs here",
        "  :       `-- previously defined here",
        "  `----"
    );
}

#[test]
fn invalid_if_missing_end_no_else() {
    let context = TestContext::default();
    let source = source_file!(&context, "begin push.1 add if.true mul");
    assert_assembler_diagnostic!(
        context,
        source,
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:29\]"#),
        "1 | begin push.1 add if.true mul",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "else", or "end", or control flow opcode (e.g. "if.true")"#
    );
}

#[test]
fn invalid_else_with_no_if() {
    let context = TestContext::default();
    let source = source_file!(&context, "begin push.1 add else mul end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:18\]"#),
        "1 | begin push.1 add else mul end",
        "  :                  ^^|^",
        "  :                    `-- found a else here",
        "  `----",
        r#" help: expected primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );

    let source = source_file!(&context, "begin push.1 while.true add else mul end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:29\]"#),
        "1 | begin push.1 while.true add else mul end end",
        "  :                             ^^|^",
        "  :                               `-- found a else here",
        "  `----",
        r#" help: expected "end""#
    );
}

#[test]
fn invalid_unmatched_else_within_if_else() {
    let context = TestContext::default();

    let source =
        source_file!(&context, "begin push.1 if.true add else mul else push.1 end end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:35\]"#),
        "1 | begin push.1 if.true add else mul else push.1 end end end",
        "  :                                   ^^|^",
        "  :                                     `-- found a else here",
        "  `----",
        r#" help: expected "end""#
    );
}

#[test]
fn invalid_if_else_no_matching_end() {
    let context = TestContext::default();

    let source = source_file!(&context, "begin push.1 add if.true mul else add");
    assert_assembler_diagnostic!(
        context,
        source,
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:38\]"#),
        "1 | begin push.1 add if.true mul else add",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );
}

#[test]
fn invalid_repeat() -> TestResult {
    let context = TestContext::default();

    // unmatched repeat
    let source = source_file!(&context, "begin push.1 add repeat.10 mul");
    assert_assembler_diagnostic!(
        context,
        source,
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:31\]"#),
        "1 | begin push.1 add repeat.10 mul",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );

    // invalid iter count
    let source = source_file!(&context, "begin push.1 add repeat.23x3 mul end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:27\]"#),
        "1 | begin push.1 add repeat.23x3 mul end end",
        "  :                           ^|",
        "  :                            `-- found a identifier here",
        "  `----",
        r#" help: expected primitive opcode (e.g. "add"), or control flow opcode (e.g. "if.true")"#
    );

    // Overflow iter count
    let count: u64 = u32::MAX as u64 + 1;
    let source = source_file!(
        &context,
        format!(
            "\
            const CONSTANT = {count}
            begin
                repeat.CONSTANT
                    add
                end
            end
            "
        )
    );
    assert_assembler_diagnostic!(
        context,
        source,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid immediate: value is larger than expected range",
        regex!(r#",-\[test[\d]+:3:24\]"#),
        "2 |             begin",
        "3 |                 repeat.CONSTANT",
        "  :                        ^^^^^^^^",
        "4 |                     add",
        "  `----"
    );
    Ok(())
}

#[test]
fn invalid_repeat_count_zero() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(&context, "begin repeat.0 nop end end");
    let error = context.assemble(source).expect_err("expected repeat.0 to be rejected");
    let rendered =
        format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
    assert!(rendered.contains("invalid repeat count"));
    Ok(())
}

#[test]
fn invalid_repeat_count_zero_with_decorator() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        "\
proc foo
    trace.1
    repeat.0
        nop
    end
end

begin
    call.foo
end"
    );
    let error = context
        .assemble(source)
        .expect_err("expected repeat.0 with decorator to be rejected");
    let rendered =
        format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
    assert!(rendered.contains("invalid repeat count"));
    Ok(())
}

#[test]
fn invalid_repeat_count_too_large() -> TestResult {
    let context = TestContext::default();
    let repeat_count = MAX_REPEAT_COUNT + 1;
    let source = source_file!(&context, format!("begin repeat.{repeat_count} nop end end"));
    let error = context
        .assemble(source)
        .expect_err("expected repeat count above limit to be rejected");
    let rendered =
        format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
    assert!(rendered.contains("invalid repeat count"));
    Ok(())
}

#[test]
fn invalid_repeat_count_constant_zero() -> TestResult {
    let context = TestContext::default();
    let source =
        source_file!(&context, "const REPEAT_COUNT = 0\nbegin repeat.REPEAT_COUNT nop end end");
    let error = context
        .assemble(source)
        .expect_err("expected repeat.0 from constant to be rejected");
    let rendered =
        format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
    assert!(rendered.contains("invalid repeat count"));
    Ok(())
}

#[test]
fn invalid_repeat_count_constant_too_large() -> TestResult {
    let context = TestContext::default();
    let repeat_count = MAX_REPEAT_COUNT + 1;
    let source = source_file!(
        &context,
        format!("const REPEAT_COUNT = {repeat_count}\nbegin repeat.REPEAT_COUNT nop end end")
    );
    let error = context
        .assemble(source)
        .expect_err("expected repeat count above limit from constant to be rejected");
    let rendered =
        format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
    assert!(rendered.contains("invalid repeat count"));
    Ok(())
}

#[test]
fn repeat_count_constant_at_limit_allowed() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        format!("const REPEAT_COUNT = {MAX_REPEAT_COUNT}\nbegin repeat.REPEAT_COUNT nop end end")
    );
    context
        .assemble(source)
        .expect("expected repeat count at limit from constant to be accepted");
    Ok(())
}

#[test]
fn const_folding_modulus_aliasing_must_be_rejected() {
    let program_src = r#"
const ALIAS = 18446744069414584320+1

begin
    push.ALIAS
end
"#;

    let assembled = Assembler::default().assemble_program(program_src);
    assert!(
        assembled.is_err(),
        "expected constants >= field modulus to be rejected (must not silently alias to 0)"
    );
}

#[test]
fn const_evaluator_modulus_aliasing_must_be_rejected() {
    let program_src = r#"
const X = 18446744069414584320
const Y = 1
const ALIAS = X+Y

begin
    push.ALIAS
end
"#;

    let assembled = Assembler::default().assemble_program(program_src);
    assert!(
        assembled.is_err(),
        "expected out-of-range constant results to be rejected (must not silently alias via `Felt::new`)"
    );
}

#[test]
fn const_folding_u64_overflow_must_not_panic_and_must_error() {
    let program_src = r#"
const WRAP = 18446744069414584320+18446744069414584320

begin
    push.WRAP
end
"#;

    let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        Assembler::default().assemble_program(program_src)
    }));

    assert!(
        assembled.is_ok(),
        "assembler panicked while folding a constant expression with u64 overflow"
    );

    let assembled = assembled.unwrap();
    assert!(
        assembled.is_err(),
        "expected the assembler to reject constant expressions which overflow u64 during folding"
    );
}

#[test]
fn const_folding_subtraction_underflow_must_be_rejected() {
    let program_src = r#"
const UNDERFLOW = 0-1

begin
    push.UNDERFLOW
end
"#;

    let assembled = Assembler::default().assemble_program(program_src);
    assert!(
        assembled.is_err(),
        "expected subtraction underflow in constant expressions to be rejected"
    );
}

#[test]
fn const_division_slash_must_not_match_int_division() {
    let program_src = r#"
const A1 = 3/2
const B1 = 3//2

const X = 3
const Y = 2
const A2 = X/Y
const B2 = X//Y

begin
    push.A1
    push.B1
    push.A2
    push.B2
end
"#;

    let program = Assembler::default()
        .assemble_program(program_src)
        .expect("program assembly must succeed");

    let entry = program.get_node_by_id(program.entrypoint()).expect("missing entrypoint node");
    let mast = format!("{}", entry.to_display(program.mast_forest()));

    let toks: Vec<&str> = mast.split_whitespace().collect();
    let pad_incr_pairs = toks.windows(2).filter(|w| w[0] == "pad" && w[1] == "incr").count();

    assert_eq!(
        pad_incr_pairs, 2,
        "expected `/` (field division) to not fold to the same value as `//` (integer division)"
    );
}

#[test]
fn const_division_by_zero_must_error() {
    let program_src = r#"
const BAD = 1/0

begin
    push.BAD
end
"#;

    let assembled = Assembler::default().assemble_program(program_src);
    assert!(
        assembled.is_err(),
        "expected division by zero in constant expressions to be rejected"
    );
}

#[test]
fn push_word_slice_u64_max_must_not_panic_and_must_error() {
    let program_src = r#"
const WORD = [1,2,3,4]

begin
    push.WORD[18446744073709551615]
end
"#;

    let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        Assembler::default().assemble_program(program_src)
    }));

    assert!(
        assembled.is_ok(),
        "assembler panicked while parsing push.WORD[...] with an out-of-range index"
    );

    let assembled = assembled.unwrap();
    assert!(
        assembled.is_err(),
        "expected push.WORD[...] with an out-of-range index to be rejected with an error"
    );
}

#[test]
fn push_word_slice_range_u64_max_end_must_not_panic_and_must_error() {
    let program_src = r#"
const WORD = [1,2,3,4]

begin
    push.WORD[0..18446744073709551615]
end
"#;

    let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        Assembler::default().assemble_program(program_src)
    }));

    assert!(
        assembled.is_ok(),
        "assembler panicked while parsing push.WORD[0..] with an out-of-range index"
    );

    let assembled = assembled.unwrap();
    assert!(
        assembled.is_err(),
        "expected push.WORD[0..] with an out-of-range index to be rejected with an error"
    );
}

#[test]
fn push_word_slice_range_u64_max_start_must_not_panic_and_must_error() {
    let program_src = r#"
const WORD = [1,2,3,4]

begin
    push.WORD[18446744073709551615..0]
end
"#;

    let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        Assembler::default().assemble_program(program_src)
    }));

    assert!(
        assembled.is_ok(),
        "assembler panicked while parsing push.WORD[..0] with an out-of-range index"
    );

    let assembled = assembled.unwrap();
    assert!(
        assembled.is_err(),
        "expected push.WORD[..0] with an out-of-range index to be rejected with an error"
    );
}

#[test]
fn invalid_while() -> TestResult {
    let context = TestContext::default();

    let source = source_file!(&context, "begin push.1 add while mul end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:24\]"#),
        "1 | begin push.1 add while mul end end",
        "  :                        ^|^",
        "  :                         `-- found a mul here",
        "  `----",
        r#" help: expected ".""#
    );

    let source = source_file!(&context, "begin push.1 add while.abc mul end end");
    assert_assembler_diagnostic!(
        context,
        source,
        "invalid syntax",
        regex!(r#",-\[test[\d]+:1:24\]"#),
        "1 | begin push.1 add while.abc mul end end",
        "  :                        ^|^",
        "  :                         `-- found a identifier here",
        "  `----",
        r#" help: expected "true""#
    );

    let source = source_file!(&context, "begin push.1 add while.true mul");
    assert_assembler_diagnostic!(
        context,
        source,
        "unexpected end of file",
        regex!(r#",-\[test[\d]+:1:32\]"#),
        "1 | begin push.1 add while.true mul",
        "  `----",
        r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
    );
    Ok(())
}

// COMPILED LIBRARIES
// ================================================================================================
#[test]
fn test_compiled_library() {
    let context = TestContext::new();
    let mut mod_parser = ModuleParser::new(ModuleKind::Library);
    let mod1 = {
        let source = source_file!(
            &context,
            "
    proc internal
        push.5
    end
    pub proc foo
        push.1
        drop
    end
    pub proc bar
        exec.internal
        drop
    end
    "
        );
        mod_parser
            .parse(PathBuf::new("mylib::mod1").unwrap(), source, context.source_manager())
            .unwrap()
    };

    let mod2 = {
        let source = source_file!(
            &context,
            "
    pub proc foo
        push.7
        add.5
    end
    # Same definition as mod1::foo
    pub proc bar
        push.1
        drop
    end
    "
        );
        mod_parser
            .parse(PathBuf::new("mylib::mod2").unwrap(), source, context.source_manager())
            .unwrap()
    };

    let compiled_library = {
        let assembler = Assembler::new(context.source_manager());
        assembler.assemble_library([mod1, mod2]).unwrap()
    };

    assert_eq!(compiled_library.exports().count(), 4);

    // Compile program that uses compiled library
    let mut assembler = Assembler::new(context.source_manager());

    assembler.link_dynamic_library(&compiled_library).unwrap();

    let program_source = "
    use mylib::mod1
    use mylib::mod2

    proc foo
        push.1
        drop
    end

    begin
        exec.mod1::foo
        exec.mod1::bar
        exec.mod2::foo
        exec.mod2::bar
        exec.foo
    end
    ";

    let _program = assembler.assemble_program(program_source).unwrap();
}

#[test]
fn test_reexported_proc_with_same_name_as_local_proc_diff_locals() {
    let context = TestContext::new();
    let mut mod_parser = ModuleParser::new(ModuleKind::Library);
    let mod1 = {
        let source = source_file!(
            &context,
            "@locals(8) pub proc foo
                push.1
                drop
            end
            "
        );
        mod_parser
            .parse(PathBuf::new("test::mod1").unwrap(), source, context.source_manager())
            .unwrap()
    };

    let mod2 = {
        let source = source_file!(
            &context,
            "use test::mod1
            pub proc foo
                exec.mod1::foo
            end
            "
        );
        mod_parser
            .parse(PathBuf::new("test::mod2").unwrap(), source, context.source_manager())
            .unwrap()
    };

    let compiled_library = {
        let assembler = Assembler::new(context.source_manager());
        assembler.assemble_library([mod1, mod2]).unwrap()
    };

    assert_eq!(compiled_library.exports().count(), 2);

    // Compile program that uses compiled library
    let mut assembler = Assembler::new(context.source_manager());

    assembler.link_dynamic_library(&compiled_library).unwrap();

    let program_source = "
    use test::mod1
    use test::mod2

    @locals(4)
    proc foo
        exec.mod1::foo
        exec.mod2::foo
    end

    begin
        exec.foo
    end
    ";

    let _program = assembler.assemble_program(program_source).unwrap();
}

// PROGRAM SERIALIZATION AND DESERIALIZATION
// ================================================================================================
#[test]
fn test_program_serde_simple() {
    let source = "
    begin
        push.1.2
        add
        drop
    end
    ";

    let assembler = Assembler::default();
    let original_program = assembler.assemble_program(source).unwrap();

    let mut target = Vec::new();
    original_program.write_into(&mut target);
    let deserialized_program = Program::read_from_bytes(&target).unwrap();

    assert_eq!(original_program, deserialized_program);
}

#[test]
fn test_program_serde_with_decorators() {
    let source = "
    const DEFAULT_CONST = 100
    const EVENT_CONST = event(\"serde::evt\")

    proc foo
        push.1.2 add
        debug.stack.8
    end

    begin
        emit.EVENT_CONST

        exec.foo

        debug.stack.4

        drop

        trace.DEFAULT_CONST
    end
    ";

    let assembler = Assembler::default();
    let original_program = assembler.assemble_program(source).unwrap();

    let mut target = Vec::new();
    original_program.write_into(&mut target);
    let deserialized_program = Program::read_from_bytes(&target).unwrap();

    assert_eq!(original_program, deserialized_program);
}

#[test]
fn vendoring() -> TestResult {
    let context = TestContext::new();
    let mut mod_parser = ModuleParser::new(ModuleKind::Library);
    let vendor_lib = {
        let source = source_file!(&context, "pub proc bar push.1 end pub proc prune push.2 end");
        let mod1 = mod_parser
            .parse(PathBuf::new("test::mod1").unwrap(), source, context.source_manager())
            .unwrap();
        Assembler::default().assemble_library([mod1]).unwrap()
    };

    let lib = {
        let source = source_file!(&context, "pub proc foo exec.::test::mod1::bar end");
        let mod2 = mod_parser
            .parse(PathBuf::new("test::mod2").unwrap(), source, context.source_manager())
            .unwrap();

        let mut assembler = Assembler::default();
        assembler.link_static_library(vendor_lib)?;
        assembler.assemble_library([mod2]).unwrap()
    };

    // Rigorous testing of vendoring functionality

    // 1. The vendored library (lib) has `exec.::test::mod1::bar` which is a 0-cycle instruction.
    // 0-cycle instructions like `exec` don't generate AssemblyOps because they don't execute
    // any VM operations. The debug info may still have procedure names, error codes, etc.
    // The vendor_lib (mod1) has actual instructions (push.1, push.2) which do have AssemblyOps.

    // 2. Create an equivalent expected library for structural comparison
    let expected_lib = {
        let source = source_file!(&context, "pub proc foo push.1 end");
        let mod2 = mod_parser.parse("test::expected", source, context.source_manager()).unwrap();
        Assembler::default().assemble_library([mod2]).unwrap()
    };

    // 3. Verify that the expected library (which has push.1) has AssemblyOps
    assert!(
        expected_lib.mast_forest().debug_info().num_asm_ops() > 0,
        "Expected library should have AssemblyOps for instruction tracking"
    );

    // 4. Verify we can create an assembler that successfully links the vendored library
    let mut assembler_with_vendored_lib = Assembler::default();
    let link_result = assembler_with_vendored_lib.link_static_library(lib.clone());
    assert!(link_result.is_ok(), "Should be able to link the vendored library");

    // 5. Test that a simple program can be assembled with the linked library
    let program_with_lib_source = r#"
    begin
        push.1
        push.2
        add
    end
    "#;
    let assemble_result = assembler_with_vendored_lib.assemble_program(program_with_lib_source);
    assert!(
        assemble_result.is_ok(),
        "Should be able to assemble program with linked library"
    );
    let assembled_program = assemble_result.unwrap();

    // Verify the assembled program has debug info (AssemblyOps)
    assert!(
        assembled_program.mast_forest().debug_info().num_asm_ops() > 0,
        "Assembled program with library should have AssemblyOps for instruction tracking"
    );

    // 6. Verify the vendored library contains the expected structure
    let mast_forest = lib.mast_forest();
    assert!(mast_forest.num_nodes() > 0, "Vendored library should have nodes");

    // Verify there are root procedures (the first node is usually a root for libraries)
    let nodes = mast_forest.nodes();
    assert!(!nodes.is_empty(), "Vendored library should have root procedures");

    Ok(())
}

// EMIT EVENT SYNTAX VALIDATION
// ================================================================================================

#[test]
fn emit_u32_immediate_is_rejected() {
    let context = TestContext::new();
    let program_source = r#"
        begin
            emit.32
        end
    "#;
    context
        .assemble(program_source)
        .expect_err(r#"emit.<u32> should be rejected; only event("...") is allowed"#);
}

#[test]
fn emit_const_must_be_event_hash() {
    let context = TestContext::new();
    // CONST defined as plain number should not be accepted by emit.CONST
    let program_source = r#"
        const BAD = 100
        begin
            emit.BAD
        end
    "#;
    context
        .assemble(program_source)
        .expect_err(r#"emit.CONST should require const defined via event("...")"#);

    // CONST defined via word("...") should also be rejected by emit.CONST
    let program_source = r#"
        const BADW = word("foo")
        begin
            emit.BADW
        end
    "#;
    context
        .assemble(program_source)
        .expect_err(r#"emit.CONST should require const defined via event("...")"#);
}

#[test]
#[should_panic]
fn test_assert_diagnostic_lines() {
    assert_diagnostic_lines!(report!("the error string"), "the error string", "other", "lines");
}

// PACKAGE SERIALIZATION AND DESERIALIZATION
// ================================================================================================

prop_compose! {
    fn any_package()(name in ".*", artifact in any::<ArbitraryMastArtifact>(), manifest in any::<PackageManifest>()) -> Package {
        let ArbitraryMastArtifact { ty, lib } = artifact;

        // Ensure the manifest reflects exports of the actual MAST artifact
        let mut exports = Vec::default();
        for export in lib.exports() {
            match export {
                LibraryExport::Procedure(export) => {
                    let digest = lib.mast_forest()[export.node].digest();
                    exports.push(PackageExport::Procedure(ProcedureExport {
                        path: export.path.clone(),digest,
                        signature: export.signature.clone(),
                        attributes: export.attributes.clone(),
                    }));
                }
                LibraryExport::Constant(export) => {
                    exports.push(PackageExport::Constant(ConstantExport {
                        path: export.path.clone(),
                        value: export.value.clone(),
                    }));
                }
                LibraryExport::Type(export) => {
                    exports.push(PackageExport::Type(TypeExport {
                        path: export.path.clone(),
                        ty: export.ty.clone(),
                    }));
                }
            }
        }

        let manifest = PackageManifest::new(exports)
            .and_then(|package_manifest| {
                package_manifest.with_dependencies(manifest.dependencies().cloned())
            })
            .expect("test package manifest should be valid");

        let name = PackageId::from(name);
        let version = miden_assembly_syntax::Version::new(0, 0, 0);
        Package { name, version, description: None, kind: ty, mast: lib, manifest, sections: Default::default() }
    }
}

#[derive(Debug, Clone)]
struct ArbitraryMastArtifact {
    ty: TargetType,
    lib: Arc<Library>,
}

impl ArbitraryMastArtifact {
    fn library(lib: Arc<Library>) -> Self {
        Self { ty: TargetType::Library, lib }
    }

    fn executable(lib: Arc<Library>) -> Self {
        Self { ty: TargetType::Executable, lib }
    }
}

impl Arbitrary for ArbitraryMastArtifact {
    type Parameters = ();

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        prop_oneof![
            Just(Self::library(LIB_EXAMPLE.clone())),
            Just(Self::executable(PRG_EXAMPLE.clone()))
        ]
        .boxed()
    }

    type Strategy = BoxedStrategy<Self>;
}

static LIB_EXAMPLE: LazyLock<Arc<Library>> = LazyLock::new(build_library_example);
static PRG_EXAMPLE: LazyLock<Arc<Library>> = LazyLock::new(build_program_example);

fn build_library_example() -> Arc<Library> {
    let context = TestContext::new();
    // declare foo module
    let foo_src = r#"
        pub proc foo(a: felt, b: felt) -> felt
            add
        end
        pub proc foo_mul(a: felt, b: felt) -> felt
            mul
        end
    "#;
    let foo_module = parse_module!(&context, "test::foo", foo_src);

    // declare bar module
    let bar_src = r#"
        pub proc bar
            mtree_get
        end
        pub proc bar_mul
            mul
        end
    "#;
    let bar_module = parse_module!(&context, "test::bar", bar_src);
    let modules = [foo_module, bar_module];

    // serialize/deserialize the bundle with locations
    Assembler::new(context.source_manager())
        .assemble_library(modules.iter().cloned())
        .expect("failed to assemble library")
}

fn build_program_example() -> Arc<Library> {
    use crate::{Parse, ParseOptions};
    let source = "
    begin
        push.1.2
        add
        drop
    end
    ";
    let assembler = Assembler::default();

    let options = ParseOptions {
        kind: ModuleKind::Executable,
        warnings_as_errors: assembler.warnings_as_errors(),
        path: Some(Path::exec_path().into()),
    };

    let program = source.parse_with_options(assembler.source_manager(), options).unwrap();
    assembler.assemble_executable_modules(program, []).unwrap().into_artifact()
}

#[test]
fn package_serialization_roundtrip() {
    // since the test is quite expensive, 128 cases should be enough to cover all edge cases
    // (default is 256)
    let cases = 128;
    TestRunner::new(Config::with_cases(cases))
        .run(&any_package(), move |package| {
            let bytes = package.to_bytes();
            let deserialized = Package::read_from_bytes(&bytes).unwrap();
            prop_assert_eq!(package, deserialized);
            Ok(())
        })
        .unwrap_or_else(|err| {
            panic!("{err}");
        });
}

// MAST TESTS
// ================================================================================================

#[test]
fn nested_blocks() -> Result<(), Report> {
    const KERNEL: &str = r#"
        pub proc foo
            add
        end"#;
    const MODULE: &str = "libs::helpers";
    const MODULE_PROCEDURE: &str = r#"
        pub proc help
            push.29
        end"#;

    let context = TestContext::new();
    let assembler = {
        let kernel_lib = Assembler::new(context.source_manager()).assemble_kernel(KERNEL).unwrap();

        let dummy_module = context.parse_module_with_path(MODULE, MODULE_PROCEDURE)?;
        let dummy_library = Assembler::new(context.source_manager())
            .assemble_library([dummy_module])
            .unwrap();

        let mut assembler = Assembler::with_kernel(context.source_manager(), kernel_lib);
        assembler.link_dynamic_library(dummy_library).unwrap();

        assembler
    };

    // The expected `MastForest` for the program (that we will build by hand)
    let mut expected_mast_forest_builder = MastForestBuilder::default();

    // fetch the kernel digest and store into a syscall block
    //
    // Note: this assumes the current internal implementation detail that `assembler.mast_forest`
    // contains the MAST nodes for the kernel after a call to
    // `Assembler::with_kernel_from_module()`.
    let syscall_foo_node_id = {
        let kernel_foo_node_id = expected_mast_forest_builder
            .ensure_block(vec![Operation::Add], Vec::new(), vec![], vec![], vec![], vec![])
            .unwrap();

        expected_mast_forest_builder
            .ensure_node(
                CallNodeBuilder::new_syscall(kernel_foo_node_id)
                    .with_before_enter(vec![])
                    .with_after_exit(vec![]),
            )
            .unwrap()
    };

    let program = r#"
    use libs::helpers

    proc foo
        push.19
    end

    proc bar
        push.17
        exec.foo
    end

    begin
        push.2
        if.true
            push.3
        else
            push.5
        end
        if.true
            if.true
                push.7
            else
                push.11
            end
        else
            push.13
            while.true
                exec.bar
                push.23
            end
        end
        exec.helpers::help
        syscall.foo
    end"#;

    let program = assembler.assemble_program(program).unwrap();

    // basic block representing foo::bar.baz procedure
    let exec_foo_bar_baz_node_id = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(29))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();

    let fmp_initialization = expected_mast_forest_builder
        .ensure_block(fmp_initialization_sequence(), Vec::new(), vec![], vec![], vec![], vec![])
        .unwrap();

    let before = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(2))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();

    let r#true1 = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(3))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();
    let r#false1 = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(5))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();
    let r#if1 = expected_mast_forest_builder
        .ensure_node(
            SplitNodeBuilder::new([r#true1, r#false1])
                .with_before_enter(vec![])
                .with_after_exit(vec![]),
        )
        .unwrap();

    let r#true3 = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(7))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();
    let r#false3 = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(11))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();
    let r#true2 = expected_mast_forest_builder
        .ensure_node(
            SplitNodeBuilder::new([r#true3, r#false3])
                .with_before_enter(vec![])
                .with_after_exit(vec![]),
        )
        .unwrap();

    let r#while = {
        let body_node_id = expected_mast_forest_builder
            .ensure_block(
                vec![
                    Operation::Push(Felt::from_u32(17)),
                    Operation::Push(Felt::from_u32(19)),
                    Operation::Push(Felt::from_u32(23)),
                ],
                Vec::new(),
                vec![],
                vec![],
                vec![],
                vec![],
            )
            .unwrap();

        expected_mast_forest_builder
            .ensure_node(
                LoopNodeBuilder::new(body_node_id)
                    .with_before_enter(vec![])
                    .with_after_exit(vec![]),
            )
            .unwrap()
    };
    let push_13_basic_block_id = expected_mast_forest_builder
        .ensure_block(
            vec![Operation::Push(Felt::from_u32(13))],
            Vec::new(),
            vec![],
            vec![],
            vec![],
            vec![],
        )
        .unwrap();

    let r#false2 = expected_mast_forest_builder
        .ensure_node(
            JoinNodeBuilder::new([push_13_basic_block_id, r#while])
                .with_before_enter(vec![])
                .with_after_exit(vec![]),
        )
        .unwrap();
    let nested = expected_mast_forest_builder
        .ensure_node(
            SplitNodeBuilder::new([r#true2, r#false2])
                .with_before_enter(vec![])
                .with_after_exit(vec![]),
        )
        .unwrap();

    let combined_node_id = expected_mast_forest_builder
        .join_nodes(
            vec![
                fmp_initialization,
                before,
                r#if1,
                nested,
                exec_foo_bar_baz_node_id,
                syscall_foo_node_id,
            ],
            None,
        )
        .unwrap();

    let (mut expected_mast_forest, node_remapping) = expected_mast_forest_builder.build();
    expected_mast_forest.make_root(node_remapping[&combined_node_id]);
    let expected_program =
        Program::new(expected_mast_forest.into(), node_remapping[&combined_node_id]);
    assert_eq!(expected_program.hash(), program.hash());

    // also check that the program has the right number of procedures (which excludes the dummy
    // library and kernel)
    assert_eq!(program.num_procedures(), 3);

    Ok(())
}

/// Ensures that the arguments of `emit` do indeed modify the digest of a basic block
#[test]
fn emit_instruction_digest() {
    let context = TestContext::new();

    let program_source = r#"
        const EVT1 = event("miden::test::event_one")
        const EVT2 = event("miden::test::event_two")

        proc foo
            emit.EVT1
        end

        proc bar
            emit.EVT2
        end

        begin
            # specific impl irrelevant
            exec.foo
            exec.bar
        end
    "#;

    let program = context.assemble(program_source).unwrap();

    let procedure_digests: Vec<Word> = program.mast_forest().procedure_digests().collect();

    // foo, bar and entrypoint
    assert_eq!(3, procedure_digests.len());

    // Ensure that foo, bar and entrypoint all have different digests
    assert_ne!(procedure_digests[0], procedure_digests[1]);
    assert_ne!(procedure_digests[0], procedure_digests[2]);
    assert_ne!(procedure_digests[1], procedure_digests[2]);
}

/// Tests that emitting events with immediate values has the same MAST representation
/// regardless of whether using emit.value or push.value emit syntax
#[test]
fn emit_syntax_equivalence() {
    let context = TestContext::new();

    // First program uses a constant
    let program1_source = r#"
        const EVT = event("miden::test::equiv")
        begin
            emit.EVT
        end
    "#;

    // Second program uses inline emit.event("...")
    let program2_source = r#"
        begin
            emit.event("miden::test::equiv")
        end
    "#;

    // Third program uses manual emit with constant event name
    let program3_source = r#"
        const EVT = event("miden::test::equiv")
        begin
            push.EVT
            emit
            drop
        end
    "#;

    let program1 = context.assemble(program1_source).unwrap();
    let program2 = context.assemble(program2_source).unwrap();
    let program3 = context.assemble(program3_source).unwrap();

    // Get the MAST forest digests for both programs
    let digest1 = program1.hash();
    let digest2 = program2.hash();
    let digest3 = program3.hash();

    // Both programs should have identical MAST representations
    assert_eq!(digest1, digest2, "MAST digests differ between programs 1 and 2");
    assert_eq!(digest1, digest3, "MAST digests differ between programs 1 and 3");

    // Verify the procedure count is 1 (just the entrypoint) for both programs
    assert_eq!(program1.num_procedures(), 1);
    assert_eq!(program2.num_procedures(), 1);
    assert_eq!(program3.num_procedures(), 1);
}

/// Since `foo` and `bar` have the same body, we only expect them to be added once to the program.
#[test]
fn duplicate_procedure() {
    let context = TestContext::new();

    let program_source = r#"
        proc foo
            add
            mul
        end

        proc bar
            add
            mul
        end

        begin
            # specific impl irrelevant
            exec.foo
            exec.bar
        end
    "#;

    let program = context.assemble(program_source).unwrap();
    // AssemblyOp metadata now participates in assembler-side deduplication. Even though
    // `foo` and `bar` have the same operations, they carry different source mappings and
    // therefore must remain distinct procedures. The entrypoint is a third procedure.
    assert_eq!(program.num_procedures(), 3);
}

#[test]
fn distinguish_grandchildren_correctly() {
    let context = TestContext::new();

    let program_source = r#"
    begin
        if.true
            while.true
                trace.1234
                push.1
            end
        end

        if.true
            while.true
                push.1
            end
        end
    end
    "#;

    let program = context.assemble(program_source).unwrap();

    let join_node = &program.mast_forest()[program.entrypoint()].unwrap_join();

    // Make sure that both `if.true` blocks compile down to a different MAST node.
    assert_ne!(join_node.first(), join_node.second());
}

#[test]
fn explicit_fully_qualified_procedure_references() -> Result<(), Report> {
    const BAR_NAME: &str = "foo::bar";
    const BAR: &str = r#"
        pub proc bar
            add
        end"#;
    const BAZ_NAME: &str = "foo::baz";
    const BAZ: &str = r#"
        pub proc baz
            exec.::foo::bar::bar
        end"#;

    let context = TestContext::default();
    let bar = context.parse_module_with_path(BAR_NAME, BAR)?;
    let baz = context.parse_module_with_path(BAZ_NAME, BAZ)?;
    let library = context.assemble_library([bar, baz]).unwrap();

    let assembler =
        Assembler::new(context.source_manager()).with_dynamic_library(&library).unwrap();

    let program = r#"
    begin
        exec.::foo::baz::baz
    end"#;

    assert_matches!(assembler.assemble_program(program), Ok(_));
    Ok(())
}

#[test]
fn re_exports() -> Result<(), Report> {
    const BAR_NAME: &str = "foo::bar";
    const BAR: &str = r#"
        pub proc baz
            add
        end"#;

    const BAZ_NAME: &str = "foo::baz";
    const BAZ: &str = r#"
        use foo::bar

        pub use bar::baz

        pub proc qux
            push.1 push.2 add
        end"#;

    let context = TestContext::new();
    let bar = context.parse_module_with_path(BAR_NAME, BAR)?;
    let baz = context.parse_module_with_path(BAZ_NAME, BAZ)?;
    let library = context.assemble_library([bar, baz]).unwrap();

    let assembler =
        Assembler::new(context.source_manager()).with_dynamic_library(&library).unwrap();

    let program = r#"
    use foo::baz

    begin
        push.1 push.2
        exec.baz::baz
        push.3 push.4
        exec.baz::qux
    end"#;

    assert_matches!(assembler.assemble_program(program), Ok(_));
    Ok(())
}

#[test]
fn module_ordering_can_be_arbitrary() -> Result<(), Report> {
    const A_NAME: &str = "a";
    const A: &str = r#"
        pub proc foo
            add
        end"#;

    const B_NAME: &str = "b";
    const B: &str = r#"
        pub proc bar
            push.1 push.2 exec.::a::foo
        end"#;

    const C_NAME: &str = "c";
    const C: &str = r#"
        pub proc baz
            exec.::b::bar
        end"#;

    let context = TestContext::new();
    let a = context.parse_module_with_path(A_NAME, A)?;
    let b = context.parse_module_with_path(B_NAME, B)?;
    let c = context.parse_module_with_path(C_NAME, C)?;

    let mut assembler = Assembler::new(context.source_manager());
    assembler.compile_and_statically_link(b)?.compile_and_statically_link(a)?;
    assembler.assemble_library([c])?;

    Ok(())
}

#[test]
fn can_assemble_a_multi_module_kernel() -> Result<(), Report> {
    const KERNEL: &str = r#"
        use kernellib::helpers->h
        pub proc foo
            exec.h::get_caller
        end"#;
    const HELPERS: &str = r#"
        pub proc get_caller
            caller
        end"#;
    const PROGRAM: &str = r#"
        begin
            syscall.foo
        end"#;

    let context = TestContext::new();

    let kernel_lib = {
        let helpers = context
            .parse_module_with_path(PathBuf::new("::kernellib::helpers").unwrap(), HELPERS)?;
        let kernel = context.parse_kernel(KERNEL).unwrap();

        let mut assembler = Assembler::new(context.source_manager());
        assembler.compile_and_statically_link(helpers)?;
        assembler.assemble_kernel(kernel).unwrap()
    };

    assert_eq!(kernel_lib.kernel().proc_hashes().len(), 1);

    Assembler::with_kernel(context.source_manager(), kernel_lib).assemble_program(PROGRAM)?;

    Ok(())
}

#[test]
fn regression_empty_kernel_library_is_rejected() {
    let context = TestContext::default();
    let source_manager = context.source_manager();

    // A kernel module with no exported procedures should be rejected.
    let kernel_masm = "pub const FOO = 1\n";
    let err = Assembler::new(source_manager)
        .assemble_kernel(kernel_masm)
        .expect_err("expected empty kernel to be rejected");
    assert_diagnostic_lines!(err, "library must contain at least one exported procedure");
}

/// Test for issue #1644: verify that single-forest merge doesn't preserves node digests
#[test]
fn issue_1644_single_forest_merge_identity() -> TestResult {
    // Test to more precisely demonstrate MastForest::merge non-identity behavior
    // This test focuses on the case where merge operation does not preserve identity for single
    // forests

    let context = TestContext::new();

    // Create a simple program that will result in specific basic block structures

    let program_source = r#"
    proc test
        push.1
        push.2
        push.3
    end

    proc main
        push.10
        if.true
            exec.test
            push.20
        else
            push.30
        end
        push.40
    end

    begin
        exec.main
    end"#;

    let program = context.assemble(program_source)?;
    let original_forest = program.mast_forest().clone();

    // Core test: Merge the forest with itself
    // This should act as identity (return the same forest) but doesn't
    let (merged_forest, _) = MastForest::merge([&*original_forest]).into_diagnostic()?;

    // Assert that the merged forest reorders nodes and both have Join nodes at expected positions
    let original_join = original_forest.nodes()[6].unwrap_join();
    let merged_join = merged_forest.nodes()[5].unwrap_join();

    // Check that they have the same structure (same first and second children, same digest)
    assert_eq!(original_join.first(), merged_join.first());
    assert_eq!(original_join.second(), merged_join.second());
    assert_eq!(original_join.digest(), merged_join.digest());

    //Assert that merging is idempotent
    let (new_merged_forest, _) = MastForest::merge([&merged_forest]).into_diagnostic()?;
    let mut should_panic = false;

    // The merge operation does not act as identity for single-element arrays
    // Check 1: Forest structure should be identical (same number of nodes)
    if new_merged_forest.nodes().len() != merged_forest.nodes().len() {
        eprintln!(
            "Forest node count differs: original={}, merged={}",
            new_merged_forest.nodes().len(),
            merged_forest.nodes().len()
        );
        eprintln!("This violates the identity requirement for merge operation");

        should_panic = true;
    }

    // Check 2: Each node should have identical digest (strict identity)
    for (i, (orig_node, merged_node)) in
        new_merged_forest.nodes().iter().zip(merged_forest.nodes().iter()).enumerate()
    {
        if orig_node.digest() != merged_node.digest() {
            eprintln!("Node {} digest violation:", i);
            eprintln!("   Original: {:?}", orig_node);
            eprintln!("   Merged:   {:?}", merged_node);
            eprintln!("   Original digest: {:?}", orig_node.digest());
            eprintln!("   Merged digest:   {:?}", merged_node.digest());

            should_panic = true;
        }
    }

    // Check 3: Roots should be identical
    for (i, (orig_root, merged_root)) in new_merged_forest
        .procedure_roots()
        .iter()
        .zip(merged_forest.procedure_roots())
        .enumerate()
    {
        if new_merged_forest[*orig_root].digest() != merged_forest[*merged_root].digest() {
            eprintln!("Root {} digest violation:", i);
            eprintln!("   Original: {:?}", original_forest[*orig_root].digest());
            eprintln!("   Merged:   {:?}", merged_forest[*merged_root].digest());
            should_panic = true;
        }
    }

    if should_panic {
        panic!("Merge idempotence violation");
    }

    eprintln!("Merge identity test passed - no violations detected");
    Ok(())
}

#[test]
fn overlong_total_path_is_rejected_without_panic() {
    use std::{
        panic::{AssertUnwindSafe, catch_unwind},
        sync::Arc,
    };

    use miden_assembly_syntax::ast::Path;

    use crate::{Parse, ParseOptions, ast::ModuleKind, testing::TestContext};

    // Build a valid path where each component is within the per-component limit (255 bytes),
    // but the total byte length exceeds u16::MAX (the binary serialization length prefix).
    let component = "a".repeat(255);
    let num_components: usize = 300;
    let mut path_str = alloc::string::String::with_capacity(num_components * (component.len() + 2));
    for i in 0..num_components {
        if i > 0 {
            path_str.push_str("::");
        }
        path_str.push_str(&component);
    }

    let context = TestContext::default();
    let source_manager = context.source_manager();

    let lib_src = r#"
pub proc add
    add.1
end
"#;

    let parsed = catch_unwind(AssertUnwindSafe(|| {
        let path = Path::new(&path_str);
        let options = ParseOptions {
            kind: ModuleKind::Library,
            warnings_as_errors: false,
            path: Some(Arc::<Path>::from(path)),
        };
        <&str as Parse>::parse_with_options(lib_src, source_manager.clone(), options)
    }));

    assert!(
        parsed.is_ok(),
        "overlong total path caused a panic; expected a structured error"
    );
    let parsed = parsed.unwrap();
    let err = parsed.expect_err("expected overlong path to be rejected");
    assert_diagnostic!(err, "invalid item path: too long");
}

#[test]
fn imported_error_message_alias_is_resolved_without_panicking() {
    use std::{
        panic::{AssertUnwindSafe, catch_unwind},
        sync::Arc,
    };

    use crate::{Assembler, DefaultSourceManager, Parse, ParseOptions, ast::ModuleKind};

    let source_manager: Arc<dyn crate::SourceManager> = Arc::new(DefaultSourceManager::default());

    // Library module `b` exports a string constant and an alias to it.
    let module_b_src = r#"
pub const ERR1 = "oops"
pub const ERR2 = ERR1
"#;
    let module_b = module_b_src
        .parse_with_options(source_manager.clone(), ParseOptions::new(ModuleKind::Library, "b"))
        .expect("module b parsing must succeed");

    // Executable module imports `ERR2` and uses it as an assertion error message.
    let module_a_src = r#"
use b::ERR2

begin
    assert.err=ERR2
end
"#;

    let assembled = catch_unwind(AssertUnwindSafe(|| {
        let mut assembler = Assembler::new(source_manager);
        assembler
            .compile_and_statically_link(module_b)
            .expect("linking module b must succeed");
        assembler.assemble_program(module_a_src)
    }));

    assert!(assembled.is_ok(), "assembler panicked during assembly");
    assembled
        .unwrap()
        .expect("expected imported error message alias to assemble successfully");
}

#[test]
fn test_issue_2181_locaddr_bug_assembly() -> TestResult {
    let context = TestContext::default();
    let source = source_file!(
        &context,
        r#"
proc some_proc
    debug.stack.4
    nop
end

@locals(4)
proc main
    locaddr.0 debug.stack.4
    locaddr.0 debug.stack.4
    locaddr.0 debug.stack.4
    exec.some_proc
    debug.stack.4
    dropw
end

begin
    exec.main
end"#
    );
    let program = context.assemble(source)?;
    insta::assert_snapshot!(program);
    Ok(())
}

/// Tests conditional debug info functionality
///
/// This test is disabled because with debug mode always enabled (issue #1821),
/// we no longer have the ability to turn debug mode off. The old functionality
#[test]
fn test_assembler_debug_info_present() {
    let context = TestContext::default();
    let source = r#"
    pub proc foo
        push.1 push.2 add
    end
    "#;

    let module = parse_module!(&context, "test::foo", source);

    // Test: With debug mode always enabled (issue #1821), debug info should always be present
    let assembler = Assembler::default();
    let library = assembler.assemble_library([module]).unwrap();
    let mast_forest = library.mast_forest();

    // Debug info should be present since debug mode is always enabled.
    // AssemblyOps are now stored separately in DebugInfo (not as Decorator::AsmOp).
    let has_asm_ops = mast_forest.debug_info().num_asm_ops() > 0;
    assert!(has_asm_ops, "AssemblyOps should be present for tracking instructions");
}

#[test]
fn test_cross_module_constant_resolution() -> TestResult {
    let context = TestContext::default();

    // Module A defines and exports a constant
    let module_a = context.parse_module_with_path(
        "cycle::module_a",
        source_file!(
            &context,
            r#"
            pub const A_VAL = 10
            pub proc a_proc
                push.A_VAL
            end
        "#
        ),
    )?;

    // Module B imports Module A and defines a constant using it
    let module_b = context.parse_module_with_path(
        "cycle::module_b",
        source_file!(
            &context,
            r#"
            use cycle::module_a
            pub const B_VAL = module_a::A_VAL + 5  # <-- Should work but fails
            pub proc b_proc
                push.B_VAL
            end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());

    let _ = assembler.assemble_library([module_a, module_b])?;

    Ok(())
}

#[test]
fn test_cross_module_constant_resolution_as_local_definition() -> TestResult {
    let context = TestContext::default();

    // Module A defines and exports a constant
    let module_a = context.parse_module_with_path(
        "cycle::module_a",
        source_file!(
            &context,
            r#"
            pub const A_VAL = 10
            pub proc a_proc
                push.A_VAL
            end
        "#
        ),
    )?;

    // Module B imports Module A and defines a constant using it
    let module_b = context.parse_module_with_path(
        "cycle::module_b",
        source_file!(
            &context,
            r#"
            use cycle::module_a::A_VAL
            pub proc b_proc
                push.A_VAL
            end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());

    let _ = assembler.assemble_library([module_a, module_b])?;

    Ok(())
}

#[test]
fn test_cross_module_constant_reexport_chain_in_procedure_scope() -> TestResult {
    let context = TestContext::new();

    let a = parse_module!(
        &context,
        "dcrc::a",
        r#"
            pub const VAL = 99
            pub proc use_val
                push.VAL
                drop
            end
        "#
    );

    let b = parse_module!(
        &context,
        "dcrc::b",
        r#"
            use dcrc::a
            pub const STEP = a::VAL + 1
            pub proc dummy
                push.STEP
                drop
            end
        "#
    );

    let c = parse_module!(
        &context,
        "dcrc::c",
        r#"
            use dcrc::b
            pub const FINAL_VAL = b::STEP + 1
            pub proc dummy
                push.FINAL_VAL
                drop
            end
        "#
    );

    let lib = Assembler::new(context.source_manager()).assemble_library([a, b, c])?;

    let src = source_file!(
        &context,
        r#"
            use dcrc::c
            const LOCAL = c::FINAL_VAL
            begin
                push.LOCAL
                drop
            end
        "#
    );

    let _program = Assembler::new(context.source_manager())
        .with_dynamic_library(lib)?
        .assemble_program(src)?;

    Ok(())
}

#[test]
fn test_issue_2696_imported_constant_with_private_dependency() -> TestResult {
    let context = TestContext::new();

    let memory = parse_module!(
        &context,
        "wallet::memory",
        r#"
            const ACCOUNT_ID_AND_NONCE_OFFSET = 4
            pub const ACCOUNT_ID_SUFFIX_OFFSET = ACCOUNT_ID_AND_NONCE_OFFSET + 2
        "#
    );

    let account = parse_module!(
        &context,
        "wallet::account",
        r#"
            use wallet::memory::ACCOUNT_ID_SUFFIX_OFFSET

            pub proc use_suffix
                push.ACCOUNT_ID_SUFFIX_OFFSET
                drop
            end
        "#
    );

    Assembler::new(context.source_manager()).assemble_library([memory, account])?;

    Ok(())
}

#[test]
fn test_cross_module_constant_cycle_in_procedure_scope_is_structured_error() {
    use std::panic::{AssertUnwindSafe, catch_unwind};

    let context = TestContext::new();

    let a = parse_module!(
        &context,
        "cycle::a",
        r#"
            use cycle::b

            pub proc use_cycle
                push.A
                drop
            end

            pub const A = b::B + 1
        "#
    );

    let b = parse_module!(
        &context,
        "cycle::b",
        r#"
            use cycle::a
            pub const B = a::A + 1
        "#
    );

    let assembled = catch_unwind(AssertUnwindSafe(|| {
        Assembler::new(context.source_manager()).assemble_library([a, b])
    }));

    assert!(assembled.is_ok(), "assembler panicked during assembly");
    let err = assembled.unwrap().expect_err("expected cyclic constants to be rejected");
    assert_diagnostic!(&err, "constant evaluation terminated due to infinite recursion");
    assert_diagnostic!(&err, "pub const A = b::B + 1");
    assert_diagnostic!(&err, "pub const B = a::A + 1");
}

#[test]
fn imported_error_message_cycle_is_rejected_without_panicking() {
    use std::panic::{AssertUnwindSafe, catch_unwind};

    let context = TestContext::new();

    let a = parse_module!(
        &context,
        "cycle::errs::a",
        r#"
            use cycle::errs::b

            pub proc use_cycle
                assert.err=ERR_A
            end

            pub const ERR_A = b::ERR_B
        "#
    );

    let b = parse_module!(
        &context,
        "cycle::errs::b",
        r#"
            use cycle::errs::a
            pub const ERR_B = a::ERR_A
        "#
    );

    let assembled = catch_unwind(AssertUnwindSafe(|| {
        Assembler::new(context.source_manager()).assemble_library([a, b])
    }));

    assert!(assembled.is_ok(), "assembler panicked during assembly");
    let err = assembled
        .unwrap()
        .expect_err("expected cyclic error message constants to be rejected");
    assert_diagnostic!(&err, "constant evaluation terminated due to infinite recursion");
    assert_diagnostic!(&err, "pub const ERR_A = b::ERR_B");
    assert_diagnostic!(&err, "pub const ERR_B = a::ERR_A");
}

#[test]
fn test_cross_module_quoted_identifier_resolution() -> TestResult {
    let context = TestContext::default();

    // Module A defines and exports a constant
    let module_a = context.parse_module_with_path(
        "cycle::\"module::a\"",
        source_file!(
            &context,
            r#"
            # Checks local path resolution
            pub proc "$item::<T>::fun"
                exec."$item::<T>::get"
            end

            # Checks absolute path resolution to a local item
            proc "$item::<T>::get"
                exec.::cycle::"module::a"::"$item::<T>::get_impl"
            end

            proc "$item::<T>::get_impl"
                push.1
            end
        "#
        ),
    )?;

    // Module B imports Module A and defines a constant using it
    let module_b = context.parse_module_with_path(
        "cycle::module::b",
        source_file!(
            &context,
            r#"
            # Checks that import resolution with quoted path components works
            use cycle::"module::a"->a

            # Checks that link-time cross-module resolution with quoted path components works
            pub proc b_proc
                exec.a::"$item::<T>::fun"
            end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());

    let _ = assembler.assemble_library([module_a, module_b])?;

    Ok(())
}

#[test]
fn test_kernel_linking_against_its_own_library() -> TestResult {
    let context = TestContext::default();

    let kernel = context.parse_kernel(source_file!(
        &context,
        r#"
        proc internal_proc
            caller
            drop
            exec.$kernel::lib::lib_proc
        end

        pub proc kernel_proc
            exec.internal_proc
        end
        "#
    ))?;

    let lib = context.parse_module_with_path(
        "$kernel::lib",
        source_file!(
            &context,
            r#"
            pub proc lib_proc
                swap
            end
            "#
        ),
    )?;

    let mut assembler = Assembler::new(context.source_manager());

    assembler.compile_and_statically_link(lib)?;

    let _ = assembler.assemble_kernel(kernel)?;

    Ok(())
}

#[test]
fn test_syscall_resolution_uses_kernel_module() -> TestResult {
    let context = TestContext::default();

    let kernel = context.parse_kernel(source_file!(
        &context,
        r#"
        pub proc foo
            caller
            drop
            push.1
        end

        pub proc bar
            caller
            drop
            push.2
        end
        "#
    ))?;

    let lib = context.parse_module_with_path(
        "userspace",
        source_file!(
            &context,
            r#"
            pub proc bar
                push.0
            end
            "#
        ),
    )?;

    let source = source_file!(
        &context,
        r#"
        use userspace::bar

        proc foo
            push.0
        end

        begin
            syscall.foo
            syscall.bar
        end
        "#
    );

    let kernel = Assembler::new(context.source_manager()).assemble_kernel(kernel)?;
    let kernel_bar_root = kernel.as_ref().get_procedure_root_by_path("::$kernel::bar").unwrap();
    let kernel_foo_root = kernel.as_ref().get_procedure_root_by_path("::$kernel::foo").unwrap();

    let mut assembler = Assembler::with_kernel(context.source_manager(), kernel);
    assembler.compile_and_statically_link(lib)?;
    let program = assembler.assemble_program(source)?;

    let mast = {
        let entry = program.get_node_by_id(program.entrypoint()).unwrap();
        format!("{}", entry.to_display(program.mast_forest()))
    };

    let expected = format!(
        r#"join
    join
        basic_block push(2147483648) push(4294967294) mstore drop noop end
        syscall.{kernel_foo_root}
    end
    syscall.{kernel_bar_root}
end"#
    );
    assert_eq!(mast, expected);

    Ok(())
}

#[test]
fn test_syscall_resolution_to_non_kernel_path_is_checked() -> TestResult {
    let context = TestContext::default();

    let kernel = context.parse_kernel(source_file!(
        &context,
        r#"
        pub proc foo
            caller
            drop
            push.1
        end
        "#
    ))?;

    let lib = context.parse_module_with_path(
        "userspace",
        source_file!(
            &context,
            r#"
            pub proc bar
                push.0
            end
            "#
        ),
    )?;

    let source = source_file!(
        &context,
        r#"
        begin
            syscall.userspace::bar
        end
        "#
    );

    let kernel = Assembler::new(context.source_manager()).assemble_kernel(kernel)?;
    let lib = Assembler::new(context.source_manager()).assemble_library([lib])?;

    let error = Assembler::with_kernel(context.source_manager(), kernel)
        .with_static_library(lib)?
        .assemble_program(source)
        .expect_err("expected diagnostic to be raised, but compilation succeeded");

    assert_diagnostic_lines!(
        error,
        "syntax error",
        "help: see emitted diagnostics for details",
        "invalid syscall: callee must be resolvable to kernel module",
        regex!(r#",-\[test[\d]+:3:21\]"#),
        "2 |         begin",
        "3 |             syscall.userspace::bar",
        "  :                     ^^^^^^^^^^^^^^",
        "4 |         end",
        "  `----"
    );

    Ok(())
}

#[test]
fn syscall_validation_does_not_panic_on_same_digest_userspace_procedure() {
    use std::panic::{AssertUnwindSafe, catch_unwind};

    let context = TestContext::default();
    let source_manager = context.source_manager();

    let kernel_src = r#"
pub proc k1
    push.1
end
"#;

    let kernel_lib = Assembler::new(source_manager.clone())
        .assemble_kernel(kernel_src)
        .expect("kernel assembly must succeed");

    let assembler = Assembler::with_kernel(source_manager, kernel_lib);

    let program_src = r#"
proc dup
    push.1
end

begin
    exec.dup
    syscall.k1
end
"#;

    let assembled = catch_unwind(AssertUnwindSafe(|| assembler.assemble_program(program_src)));
    assert!(assembled.is_ok(), "assembler panicked while assembling a valid program");
    assert!(assembled.unwrap().is_ok(), "expected program assembly to succeed");
}

#[test]
fn syscall_by_unknown_digest_is_rejected_at_assembly_time_when_kernel_is_configured() {
    let context = TestContext::default();
    let source_manager = context.source_manager();

    let kernel_src = r#"
pub proc k1
    push.1
end
"#;

    let kernel_lib = Assembler::new(source_manager.clone())
        .assemble_kernel(kernel_src)
        .expect("kernel assembly must succeed");

    let assembler = Assembler::with_kernel(source_manager, kernel_lib);

    let program_src = r#"
begin
    syscall.0x0000000000000000000000000000000000000000000000000000000000000000
end
"#;

    let err = assembler
        .assemble_program(program_src)
        .expect_err("expected unknown digest syscall to be rejected");
    assert_diagnostic!(err, "invalid syscall");
}

#[test]
fn syscall_without_kernel_is_rejected_at_assembly_time() {
    let context = TestContext::default();
    let assembler = Assembler::new(context.source_manager());

    let program_src = r#"
begin
    syscall.0x0000000000000000000000000000000000000000000000000000000000000000
end
"#;

    let err = assembler
        .assemble_program(program_src)
        .expect_err("expected syscall without kernel to be rejected");
    assert_diagnostic!(err, "invalid syscall");
}

#[test]
fn test_linking_imported_symbols_with_duplicate_prefix_components() -> TestResult {
    let context = TestContext::default();

    // The name of this library is `lib::lib` on purpose
    let lib = context.parse_module_with_path(
        "lib::lib",
        source_file!(
            &context,
            r#"
        pub proc lib_proc
            swap
        end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());
    let lib = assembler.assemble_library([lib])?;

    // This program triggers a pathological edge case in symbol resolution, caused by the
    // import-relative reference `exec.lib::lib_proc`. This causes the following to occur:
    //
    // 1. We attempt to expand `lib` to an import, which succeeds, giving us a new relative path,
    //    `lib::lib::lib_proc`.
    // 2. Because imports can refer to other imported items, we attempt to resolve `lib` as an
    //    import again, resulting in us expanding the path to `lib::lib::lib::lib::lib_proc`, and so
    //    on until we blow the stack
    //
    // The fix for this is to disregard import expansions of the same import in the same module
    // after the first time an import is expanded.
    let assembler = Assembler::new(context.source_manager());
    let _ = assembler.with_static_library(lib)?.assemble_program(
        r#"
        use lib::lib

        begin
            exec.lib::lib_proc
        end
        "#,
    )?;

    Ok(())
}

#[test]
#[ignore = "leave disabled until either symbol resolution is rewritten or path semantics are refined"]
fn test_linking_recursive_expansion() -> TestResult {
    let context = TestContext::default();

    let a_lib = context.parse_module_with_path(
        "a",
        source_file!(
            &context,
            r#"
        pub use b::a
        pub proc x
            push.1
        end
        "#
        ),
    )?;

    let b_lib = context.parse_module_with_path(
        "b",
        source_file!(
            &context,
            r#"
        pub use a::a
        pub proc foo
            exec.a::x
        end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());
    let _ = assembler.assemble_library([a_lib, b_lib])?;

    Ok(())
}

#[test]
#[ignore = "leave disabled until either symbol resolution is rewritten or path semantics are refined"]
fn test_linking_recursive_expansion_via_renamed_aliases() -> TestResult {
    let context = TestContext::default();

    let a_lib = context.parse_module_with_path(
        "a::a",
        source_file!(
            &context,
            r#"
        pub use b::a2
        pub proc x
            push.1
        end
        "#
        ),
    )?;

    let b_lib = context.parse_module_with_path(
        "b",
        source_file!(
            &context,
            r#"
        pub use a::a->a2
        pub proc foo
            exec.a2::x
        end
        "#
        ),
    )?;

    let assembler = Assembler::new(context.source_manager());
    let _ = assembler.assemble_library([a_lib, b_lib])?;

    Ok(())
}
