// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#[cfg(test)]
mod circuit_classical_ctl_tests;
#[cfg(test)]
mod circuit_tests;
mod debug;
#[cfg(test)]
mod debugger_tests;
#[cfg(test)]
mod package_tests;
#[cfg(test)]
mod tests;

use std::{cell::RefCell, rc::Rc};

use crate::{
    error::{self, WithStack},
    incremental::Compiler,
    location::Location,
};
use debug::format_call_stack;
use miette::Diagnostic;
use num_bigint::BigUint;
use num_complex::Complex;
use qdk_simulators::noise_config::NoiseConfig;
use qsc_circuit::{
    Circuit, CircuitTracer, TracerConfig,
    operations::{entry_expr_for_qubit_operation, qubit_param_info},
    rir_to_circuit::rir_to_circuit,
};
use qsc_codegen::qir::{
    fir_to_qir, fir_to_qir_from_callable, fir_to_rir, fir_to_rir_from_callable,
};
use qsc_data_structures::{
    error::WithSource,
    functors::FunctorApp,
    language_features::LanguageFeatures,
    line_column::{Encoding, Range},
    source::{Source, SourceMap},
    span::Span,
    target::{Profile, TargetCapabilityFlags},
};
use qsc_eval::{
    Env, ErrorBehavior, State, VariableInfo,
    backend::{Backend, CliffordSim, SparseSim, TracingBackend},
    output::Receiver,
};
pub use qsc_eval::{
    StepAction, StepResult,
    debug::Frame,
    noise::PauliNoise,
    output::{self, GenericReceiver},
    val::Closure,
    val::Range as ValueRange,
    val::Result,
    val::Value,
};
use qsc_fir::{
    fir::{
        self, Block, BlockId, ExecGraph, ExecGraphConfig, Expr, ExprId, Global, Package, PackageId,
        PackageStoreLookup, Pat, PatId, Stmt, StmtId,
    },
    visit::{self, Visitor},
};
use qsc_frontend::{
    compile::{CompileUnit, Dependencies, PackageStore},
    incremental::Increment,
};
use qsc_hir::{global, ty};
use qsc_linter::{HirLint, Lint, LintKind, LintLevel};
use qsc_lowerer::{
    map_fir_local_item_to_hir, map_fir_package_to_hir, map_hir_local_item_to_fir,
    map_hir_package_to_fir,
};
use qsc_partial_eval::{PartialEvalConfig, ProgramEntry};
use qsc_passes::{PackageType, PassContext};
use qsc_rca::PackageStoreComputeProperties;
use rustc_hash::FxHashSet;
use thiserror::Error;

impl Error {
    #[must_use]
    pub fn stack_trace(&self) -> Option<&String> {
        match &self {
            Error::Eval(err) => err.stack_trace(),
            _ => None,
        }
    }
}

#[derive(Clone, Debug, Diagnostic, Error)]
pub enum Error {
    #[error(transparent)]
    #[diagnostic(transparent)]
    Compile(#[from] crate::compile::Error),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Pass(#[from] WithSource<qsc_passes::Error>),
    #[error("runtime error")]
    #[diagnostic(transparent)]
    Eval(#[from] WithStack<WithSource<qsc_eval::Error>>),
    #[error("circuit error")]
    #[diagnostic(transparent)]
    Circuit(#[from] qsc_circuit::Error),
    #[error("entry point not found")]
    #[diagnostic(code("Qsc.Interpret.NoEntryPoint"))]
    NoEntryPoint,
    #[error("unsupported runtime capabilities for code generation")]
    #[diagnostic(code("Qsc.Interpret.UnsupportedRuntimeCapabilities"))]
    UnsupportedRuntimeCapabilities,
    #[error("expression does not evaluate to an operation")]
    #[diagnostic(code("Qsc.Interpret.NotAnOperation"))]
    #[diagnostic(help("provide the name of a callable or a lambda expression"))]
    NotAnOperation,
    #[error("value is not a global callable")]
    #[diagnostic(code("Qsc.Interpret.NotACallable"))]
    NotACallable,
    #[error("partial evaluation error")]
    #[diagnostic(transparent)]
    PartialEvaluation(#[from] WithSource<qsc_partial_eval::Error>),
}

#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
pub enum SimType {
    #[default]
    Sparse,
    Clifford(usize),
}

/// A Q# interpreter.
pub struct Interpreter {
    /// The incremental Q# compiler.
    compiler: Compiler,
    /// The target capabilities used for compilation.
    capabilities: TargetCapabilityFlags,
    /// The computed properties for the package store, if any, used for code generation.
    compute_properties: Option<PackageStoreComputeProperties>,
    /// The number of lines that have so far been compiled.
    /// This field is used to generate a unique label
    /// for each line evaluated with `eval_fragments`.
    lines: u32,
    // The FIR store
    fir_store: fir::PackageStore,
    /// FIR lowerer
    lowerer: qsc_lowerer::Lowerer,
    /// The execution graph for the last expression evaluated.
    expr_graph: Option<ExecGraph>,
    /// Checking if an `ItemId` corresponds to the `Std.OpenQASM.Angle.Angle` UDT
    /// is an expensive operation. So, we cache the id to avoid incurring that cost.
    angle_ty_cache: RefCell<Option<crate::hir::ItemId>>,
    /// Checking if an `ItemId` corresponds to the `Std.Math.Complex` UDT
    /// is an expensive operation. So, we cache the id to avoid incurring that cost.
    complex_ty_cache: RefCell<Option<crate::hir::ItemId>>,
    /// The ID of the current package.
    /// This ID is valid both for the FIR store and the `PackageStore`.
    package: PackageId,
    /// The ID of the source package. The source package
    /// is made up of the initial sources passed in when creating the interpreter.
    /// This ID is valid both for the FIR store and the `PackageStore`.
    source_package: PackageId,
    /// The default simulator backend.
    sim: SparseSim,
    /// When circuit tracing is enabled, the tracer that records the circuit during evaluation.
    circuit_tracer: Option<CircuitTracer>,
    /// The quantum seed, if any. This is cached here so that it can be used in calls to
    /// `run_internal` which use a passed instance of the simulator instead of the one above.
    quantum_seed: Option<u64>,
    /// The classical seed, if any. This needs to be passed to the evaluator for use in intrinsic
    /// calls that produce classical random numbers.
    classical_seed: Option<u64>,
    /// The evaluator environment.
    env: Env,
    /// The execution graph configuration to use for evaluation.
    eval_config: ExecGraphConfig,
}

pub type InterpretResult = std::result::Result<Value, Vec<Error>>;

/// Indicates whether an UDT is an `OpenQASM` `Angle` or a `Complex` number.
/// This information is needed in the Python interop layer to give special
/// treatment to the instances of these UDTs.
pub enum UdtKind {
    /// `Std.OpenQASM.Angle.Angle`
    Angle,
    /// `Std.Math.Complex`
    Complex,
    /// A normal UDT, see the other variants for the special cases.
    Udt,
}

/// An item tagged with its name and the namespace it was defined in.
pub struct TaggedItem {
    pub item_id: qsc_hir::hir::ItemId,
    pub name: Rc<str>,
    pub namespace: Vec<Rc<str>>,
}

#[derive(PartialEq, Eq, Copy, Clone)]
pub enum TraceCircuitOption {
    Enabled,
    Disabled,
}

impl Interpreter {
    /// Creates a new incremental compiler, compiling the passed in sources.
    /// # Errors
    /// If compiling the sources fails, compiler errors are returned.
    pub fn new(
        sources: SourceMap,
        package_type: PackageType,
        capabilities: TargetCapabilityFlags,
        language_features: LanguageFeatures,
        store: PackageStore,
        dependencies: &Dependencies,
    ) -> std::result::Result<Self, Vec<Error>> {
        Self::with_sources(
            ExecGraphConfig::NoDebug,
            sources,
            package_type,
            capabilities,
            language_features,
            store,
            dependencies,
            None,
        )
    }

    pub fn with_circuit_trace(
        sources: SourceMap,
        package_type: PackageType,
        capabilities: TargetCapabilityFlags,
        language_features: LanguageFeatures,
        store: PackageStore,
        dependencies: &Dependencies,
        circuit_tracer_config: TracerConfig,
    ) -> std::result::Result<Self, Vec<Error>> {
        Self::with_sources(
            ExecGraphConfig::NoDebug,
            sources,
            package_type,
            capabilities,
            language_features,
            store,
            dependencies,
            Some(circuit_tracer_config),
        )
    }

    /// Creates a new incremental compiler with debugging stmts enabled, compiling the passed in sources.
    /// # Errors
    /// If compiling the sources fails, compiler errors are returned.
    pub fn with_debug(
        sources: SourceMap,
        package_type: PackageType,
        capabilities: TargetCapabilityFlags,
        language_features: LanguageFeatures,
        store: PackageStore,
        dependencies: &Dependencies,
        trace_circuit_config: TracerConfig,
    ) -> std::result::Result<Self, Vec<Error>> {
        Self::with_sources(
            ExecGraphConfig::Debug,
            sources,
            package_type,
            capabilities,
            language_features,
            store,
            dependencies,
            Some(trace_circuit_config),
        )
    }

    #[allow(clippy::too_many_arguments)]
    fn with_sources(
        eval_config: ExecGraphConfig,
        sources: SourceMap,
        package_type: PackageType,
        capabilities: TargetCapabilityFlags,
        language_features: LanguageFeatures,
        store: PackageStore,
        dependencies: &Dependencies,
        circuit_tracer_config: Option<TracerConfig>,
    ) -> std::result::Result<Self, Vec<Error>> {
        let compiler = Compiler::new(
            sources,
            package_type,
            capabilities,
            language_features,
            store,
            dependencies,
        )
        .map_err(into_errors)?;

        Self::with_compiler(eval_config, capabilities, circuit_tracer_config, compiler)
    }

    pub fn with_package_store(
        dbg: bool,
        store: PackageStore,
        source_package_id: qsc_hir::hir::PackageId,
        capabilities: TargetCapabilityFlags,
        language_features: LanguageFeatures,
        dependencies: &Dependencies,
    ) -> std::result::Result<Self, Vec<Error>> {
        let compiler = Compiler::with_package_store(
            store,
            source_package_id,
            capabilities,
            language_features,
            dependencies,
        )
        .map_err(into_errors)?;

        // Always enable circuit tracing along with debugging.
        let circuit_tracer_config = if dbg {
            Some(Debugger::circuit_config())
        } else {
            None
        };

        let eval_config = if dbg {
            ExecGraphConfig::Debug
        } else {
            ExecGraphConfig::NoDebug
        };

        Self::with_compiler(eval_config, capabilities, circuit_tracer_config, compiler)
    }

    fn with_compiler(
        eval_config: ExecGraphConfig,
        capabilities: TargetCapabilityFlags,
        circuit_tracer_config: Option<TracerConfig>,
        compiler: Compiler,
    ) -> std::result::Result<Interpreter, Vec<Error>> {
        let mut fir_store = fir::PackageStore::new();
        for (id, unit) in compiler.package_store() {
            let mut lowerer = qsc_lowerer::Lowerer::new();
            let pkg = lowerer.lower_package(&unit.package, &fir_store);
            fir_store.insert(map_hir_package_to_fir(id), pkg);
        }

        let source_package_id = compiler.source_package_id();
        let package_id = compiler.package_id();

        let package = map_hir_package_to_fir(package_id);
        let compute_properties = if capabilities == TargetCapabilityFlags::all() {
            None
        } else {
            let compute_properties = PassContext::run_fir_passes_on_fir(
                &fir_store,
                map_hir_package_to_fir(source_package_id),
                capabilities,
            )
            .map_err(|caps_errors| {
                let source_package = compiler
                    .package_store()
                    .get(source_package_id)
                    .expect("package should exist in the package store");

                caps_errors
                    .into_iter()
                    .map(|error| Error::Pass(WithSource::from_map(&source_package.sources, error)))
                    .collect::<Vec<_>>()
            })?;

            Some(compute_properties)
        };

        Ok(Self {
            compiler,
            lines: 0,
            capabilities,
            compute_properties,
            fir_store,
            lowerer: qsc_lowerer::Lowerer::new(),
            expr_graph: None,
            angle_ty_cache: None.into(),
            complex_ty_cache: None.into(),
            env: Env::default(),
            sim: SparseSim::new(),
            circuit_tracer: circuit_tracer_config.map(|config| {
                CircuitTracer::new(
                    config,
                    &[package, map_hir_package_to_fir(source_package_id)],
                )
            }),
            quantum_seed: None,
            classical_seed: None,
            package,
            source_package: map_hir_package_to_fir(source_package_id),
            eval_config,
        })
    }

    /// Given a package ID, returns all the global items in the package.
    /// Note this does not currently include re-exports.
    fn package_globals(&self, package_id: PackageId) -> Vec<(Vec<Rc<str>>, Rc<str>, Value)> {
        let mut exported_items = Vec::new();
        let package = &self
            .compiler
            .package_store()
            .get(map_fir_package_to_hir(package_id))
            .expect("package should exist in the package store")
            .package;
        for global in global::iter_package(map_fir_package_to_hir(package_id), package) {
            if let global::Kind::Callable(term) = global.kind {
                let store_item_id = fir::StoreItemId {
                    package: package_id,
                    item: fir::LocalItemId::from(usize::from(term.id.item)),
                };
                exported_items.push((
                    global.namespace,
                    global.name,
                    Value::Global(store_item_id, FunctorApp::default()),
                ));
            }
        }
        exported_items
    }

    /// Get the global callables defined in the user source passed into initialization of the interpreter as `Value` instances.
    pub fn source_globals(&self) -> Vec<(Vec<Rc<str>>, Rc<str>, Value)> {
        self.package_globals(self.source_package)
    }

    /// Get the global callables defined in the open package being interpreted as `Value` instances, which will include any items
    /// defined by calls to `eval_fragments` and the like.
    pub fn user_globals(&self) -> Vec<(Vec<Rc<str>>, Rc<str>, Value)> {
        self.package_globals(self.package)
    }

    /// Get the input and output types of a given value representing a global item.
    /// # Panics
    /// Panics if the item is not callable or a type that can be invoked as a callable.
    pub fn global_callable_ty(&self, item_id: &Value) -> Option<(ty::Ty, ty::Ty)> {
        let (item_id, is_closure) = match item_id {
            Value::Global(item_id, _) => (*item_id, false),
            Value::Closure(closure) => (closure.id, true),
            _ => panic!("value is not a callable"),
        };

        let package_id = map_fir_package_to_hir(item_id.package);
        let unit = self
            .compiler
            .package_store()
            .get(package_id)
            .expect("package should exist in the package store");
        let item = unit
            .package
            .items
            .get(qsc_hir::hir::LocalItemId::from(usize::from(item_id.item)))?;
        match &item.kind {
            qsc_hir::hir::ItemKind::Callable(decl) => {
                if is_closure {
                    // The arguments are a tuple where the first element is the input arguments and
                    // the second are the captured variables.
                    // Grab that first element to get the actual input type.
                    let ty::Ty::Tuple(elems) = &decl.input.ty else {
                        panic!("closure input type is not a tuple")
                    };
                    let input_ty = elems
                        .last()
                        .expect("closure input type should have at least one element");
                    Some((input_ty.clone(), decl.output.clone()))
                } else {
                    Some((decl.input.ty.clone(), decl.output.clone()))
                }
            }
            qsc_hir::hir::ItemKind::Ty(_, udt) => {
                // We don't handle UDTs, so we return an error type that prevents later code from processing this item.
                Some((udt.get_pure_ty(), ty::Ty::Err))
            }
            _ => panic!("item is not callable"),
        }
    }

    /// Given a package ID, returns all the types in the package.
    /// Note this does not currently include re-exports.
    fn package_types(&self, package_id: PackageId) -> Vec<TaggedItem> {
        let mut exported_items = Vec::new();
        let package = &self
            .compiler
            .package_store()
            .get(map_fir_package_to_hir(package_id))
            .expect("package should exist in the package store")
            .package;
        for global in global::iter_package(map_fir_package_to_hir(package_id), package) {
            if let global::Kind::Ty(ty) = global.kind {
                exported_items.push(TaggedItem {
                    item_id: ty.id,
                    name: global.name,
                    namespace: global.namespace,
                });
            }
        }
        exported_items
    }

    /// Get the global UDTs defined in the user source passed into initialization of the interpreter.
    pub fn source_types(&self) -> Vec<TaggedItem> {
        self.package_types(self.source_package)
    }

    /// Get the global UDTs defined in the open package being interpreted, which will include any items
    /// defined by calls to `eval_fragments` and the like.
    pub fn user_types(&self) -> Vec<TaggedItem> {
        self.package_types(self.package)
    }

    pub fn udt_ty_from_store_item_id(
        &self,
        store_item_id: crate::fir::StoreItemId,
    ) -> (&ty::Udt, UdtKind) {
        self.udt_ty_from_item_id(&crate::hir::ItemId {
            package: map_fir_package_to_hir(store_item_id.package),
            item: map_fir_local_item_to_hir(store_item_id.item),
        })
    }

    /// Get the type of a UDT given its `item_id`.
    /// # Panics
    /// Panics if the item is not a UDT.
    pub fn udt_ty_from_item_id(&self, item_id: &crate::hir::ItemId) -> (&ty::Udt, UdtKind) {
        let crate::hir::ItemId {
            package: package_id,
            item: local_item_id,
        } = item_id;

        let unit = self
            .compiler
            .package_store()
            .get(*package_id)
            .expect("package should exist in the package store");

        let item = unit
            .package
            .items
            .get(*local_item_id)
            .expect("item should be in this package");

        let parent = item.parent.map(|parent| {
            &unit
                .package
                .items
                .get(parent)
                .expect("parent should exist")
                .kind
        });

        let qsc_hir::hir::ItemKind::Ty(_, udt) = &item.kind else {
            panic!("item is not a UDT")
        };

        let kind = if let Some(id) = &*self.angle_ty_cache.borrow()
            && id == item_id
        {
            UdtKind::Angle
        } else if let Some(id) = &*self.complex_ty_cache.borrow()
            && id == item_id
        {
            UdtKind::Complex
        } else if let Some(qsc_hir::hir::ItemKind::Namespace(namespace, _)) = parent {
            let namespace: Vec<_> = namespace.into();
            let namespace: Vec<&str> = namespace.iter().map(|ident| &**ident).collect();
            if matches!(&namespace[..], &["Std", "OpenQASM", "Angle"]) && &*udt.name == "Angle" {
                *self.angle_ty_cache.borrow_mut() = Some(*item_id);
                UdtKind::Angle
            } else if matches!(&namespace[..], &["Std", "Core"]) && &*udt.name == "Complex" {
                *self.complex_ty_cache.borrow_mut() = Some(*item_id);
                UdtKind::Complex
            } else {
                UdtKind::Udt
            }
        } else {
            UdtKind::Udt
        };

        (udt, kind)
    }

    /// Returns the [`fir::StoreItemId`] for the `Std.OpenQASM.Angle.Angle` UDT.
    ///
    /// This function intended to be used from
    /// `source/pip/src/interpreter/data_interop.rs::pyobj_to_value`
    /// to tag the angles coming from Python with the correct `StoreItemId`.
    pub fn get_angle_id(&self) -> fir::StoreItemId {
        if let Some(id) = &*self.angle_ty_cache.borrow() {
            let crate::hir::ItemId {
                package: hir_package_id,
                item: hir_local_item_id,
            } = id;
            let fir_package_id = map_hir_package_to_fir(*hir_package_id);
            let fir_local_item_id = map_hir_local_item_to_fir(*hir_local_item_id);
            crate::fir::StoreItemId {
                package: fir_package_id,
                item: fir_local_item_id,
            }
        } else {
            // SAFETY: This function is intended to be used when receiving Python objects
            //         in the interop layer. The only way to send a Python object to Q# is
            //         as the argument of a function call. When performing type checking
            //         for this function call in the interop layer, there are two cases:
            //
            //           1. The input type is not `Std.OpenQASM.Angle.Angle` and we return
            //              an error.
            //           2. The input type is `Std.OpenQASM.Angle.Angle`. To verify that
            //              the input type is indeed `Angle`, we call `udt_ty_from_item_id`,
            //              which caches the `Angle` UDT's `LocalItemId`.
            //
            //         So, if we proceed to execute the function's body, it's guaranteed
            //         that we have already cached `Std.OpenQASM.Angle.Angle`'s `LocalItemId`.
            //         Therefore, this else-branch is unreachable.
            unreachable!("`self.angle_ty_cache` should be set by `udt_ty_from_item_id`")
        }
    }

    /// Returns the [`fir::StoreItemId`] for the `Std.Math.Complex` UDT.
    ///
    /// This function intended to be used from
    /// `source/pip/src/interpreter/data_interop.rs::pyobj_to_value`
    /// to tag the complex numbers coming from Python with the correct
    /// `StoreItemId`.
    pub fn get_complex_id(&self) -> crate::fir::StoreItemId {
        if let Some(id) = &*self.complex_ty_cache.borrow() {
            let crate::hir::ItemId {
                package: hir_package_id,
                item: hir_local_item_id,
            } = id;
            let fir_package_id = map_hir_package_to_fir(*hir_package_id);
            let fir_local_item_id = map_hir_local_item_to_fir(*hir_local_item_id);
            crate::fir::StoreItemId {
                package: fir_package_id,
                item: fir_local_item_id,
            }
        } else {
            // SAFETY: This function is intended to be used when receiving Python objects
            //         in the interop layer. The only way to send a Python object to Q# is
            //         as the argument of a function call. When performing type checking
            //         for this function call in the interop layer, there are two cases:
            //
            //           1. The input type is not `Std.Math.Complex` and we return an error.
            //           2. The input type is `Std.Math.Complex`. To verify that the input
            //              type is indeed `Complex`, we call `udt_ty_from_item_id`, which
            //              caches the `Complex` UDT's `LocalItemId`.
            //
            //         So, if we proceed to execute the function's body, it's guaranteed
            //         that we have already cached `Std.Math.Complex`'s `LocalItemId`.
            //         Therefore, this else-branch is unreachable.
            unreachable!("`self.complex_ty_cache` should be set by `udt_ty_from_item_id`")
        }
    }

    pub fn set_quantum_seed(&mut self, seed: Option<u64>) {
        self.quantum_seed = seed;
        self.sim.set_seed(seed);
    }

    pub fn set_classical_seed(&mut self, seed: Option<u64>) {
        self.classical_seed = seed;
    }

    pub fn check_source_lints(&self) -> Vec<Lint> {
        if let Some(compile_unit) = self
            .compiler
            .package_store()
            .get(self.compiler.source_package_id())
        {
            qsc_linter::run_lints(
                self.compiler.package_store(),
                compile_unit,
                // see https://github.com/microsoft/qdk/pull/1627 for context
                // on why we override this config
                Some(&[qsc_linter::LintOrGroupConfig::Lint(
                    qsc_linter::LintConfig {
                        kind: LintKind::Hir(HirLint::NeedlessOperation),
                        level: LintLevel::Warn,
                    },
                )]),
            )
        } else {
            Vec::new()
        }
    }

    /// Executes the entry expression until the end of execution.
    /// # Errors
    /// Returns a vector of errors if evaluating the entry point fails.
    pub fn eval_entry(&mut self, receiver: &mut impl Receiver) -> InterpretResult {
        let graph = self.get_entry_exec_graph()?;
        self.expr_graph = Some(graph.clone());
        eval(
            self.source_package,
            self.classical_seed,
            graph,
            self.eval_config,
            self.compiler.package_store(),
            &self.fir_store,
            &mut Env::default(),
            &mut TracingBackend::new(&mut self.sim, self.circuit_tracer.as_mut()),
            receiver,
        )
    }

    /// Executes the entry expression until the end of execution, using the given simulator backend
    /// and a new instance of the environment.
    pub fn eval_entry_with_sim(
        &mut self,
        sim: &mut impl Backend,
        receiver: &mut impl Receiver,
    ) -> InterpretResult {
        let graph = self.get_entry_exec_graph()?;
        self.expr_graph = Some(graph.clone());
        if self.quantum_seed.is_some() {
            sim.set_seed(self.quantum_seed);
        }
        eval(
            self.source_package,
            self.classical_seed,
            graph,
            self.eval_config,
            self.compiler.package_store(),
            &self.fir_store,
            &mut Env::default(),
            &mut TracingBackend::no_tracer(sim),
            receiver,
        )
    }

    fn get_entry_exec_graph(&self) -> std::result::Result<ExecGraph, Vec<Error>> {
        let unit = self.fir_store.get(self.source_package);
        if unit.entry.is_some() {
            return Ok(unit.entry_exec_graph.clone());
        }
        Err(vec![Error::NoEntryPoint])
    }

    /// # Errors
    /// If the parsing of the fragments fails, an error is returned.
    /// If the compilation of the fragments fails, an error is returned.
    /// If there is a runtime error when interpreting the fragments, an error is returned.
    pub fn eval_fragments(
        &mut self,
        receiver: &mut impl Receiver,
        fragments: &str,
    ) -> InterpretResult {
        let label = self.next_line_label();

        let mut increment = self
            .compiler
            .compile_fragments_fail_fast(&label, fragments)
            .map_err(into_errors)?;

        // Clear the entry expression, as we are evaluating fragments and a fragment with a `@EntryPoint` attribute
        // should not change what gets executed.
        increment.clear_entry();

        self.eval_increment(receiver, increment)
    }

    /// It is assumed that if there were any parse errors on the fragments, the caller would have
    /// already handled them. This function is intended to be used in cases where the caller wants
    /// to handle the parse errors themselves.
    ///  # Errors
    /// If the compilation of the fragments fails, an error is returned.
    /// If there is a runtime error when interpreting the fragments, an error is returned.
    pub fn eval_ast_fragments(
        &mut self,
        receiver: &mut impl Receiver,
        fragments: &str,
        package: qsc_ast::ast::Package,
    ) -> InterpretResult {
        let label = self.next_line_label();

        let increment = self
            .compiler
            .compile_ast_fragments_fail_fast(&label, fragments, package)
            .map_err(into_errors)?;

        self.eval_increment(receiver, increment)
    }

    fn eval_increment(
        &mut self,
        receiver: &mut impl Receiver,
        increment: Increment,
    ) -> InterpretResult {
        let (graph, _) = self.lower(&increment)?;
        self.expr_graph = Some(graph.clone());

        // Updating the compiler state with the new AST/HIR nodes
        // is not necessary for the interpreter to function, as all
        // the state required for evaluation already exists in the
        // FIR store. It could potentially save some memory
        // *not* to do hold on to the AST/HIR, but it is done
        // here to keep the package stores consistent.
        self.compiler.update(increment);

        eval(
            self.package,
            self.classical_seed,
            graph,
            self.eval_config,
            self.compiler.package_store(),
            &self.fir_store,
            &mut self.env,
            &mut TracingBackend::new(&mut self.sim, self.circuit_tracer.as_mut()),
            receiver,
        )
    }

    /// Invokes the given callable with the given arguments using the current environment, simulator, and compilation.
    pub fn invoke(
        &mut self,
        receiver: &mut impl Receiver,
        callable: Value,
        args: Value,
    ) -> InterpretResult {
        qsc_eval::invoke(
            self.package,
            self.classical_seed,
            &self.fir_store,
            self.eval_config,
            &mut self.env,
            &mut TracingBackend::new(&mut self.sim, self.circuit_tracer.as_mut()),
            receiver,
            callable,
            args,
        )
        .map_err(|(error, call_stack)| {
            eval_error(
                self.compiler.package_store(),
                &self.fir_store,
                call_stack,
                error,
            )
        })
    }

    // Invokes the given callable with the given arguments using the current compilation but with a fresh
    // environment and simulator configured with the given noise, if any.
    #[allow(clippy::too_many_arguments)]
    pub fn invoke_with_noise(
        &mut self,
        receiver: &mut impl Receiver,
        callable: Value,
        args: Value,
        noise: Option<PauliNoise>,
        qubit_loss: Option<f64>,
        noise_config: Option<NoiseConfig<f64, f64>>,
        seed: Option<u64>,
        sim_type: SimType,
    ) -> InterpretResult {
        let qubit_loss = if noise_config.is_none() {
            qubit_loss
        } else {
            None
        };

        match sim_type {
            SimType::Sparse => {
                let mut sim = match noise {
                    Some(noise) => SparseSim::new_with_noise(&noise),
                    None => match noise_config {
                        Some(config) => SparseSim::new_with_noise_config(config.into()),
                        None => SparseSim::new(),
                    },
                };
                if let Some(loss) = qubit_loss {
                    sim.set_loss(loss);
                }
                if seed.is_some() {
                    sim.set_seed(seed);
                }
                self.invoke_with_sim(&mut sim, receiver, callable, args, seed)
            }
            SimType::Clifford(num_qubits) => {
                let mut sim = match noise_config {
                    Some(config) => CliffordSim::new_with_noise_config(num_qubits, config.into()),
                    None => CliffordSim::new(num_qubits),
                };
                if seed.is_some() {
                    sim.set_seed(seed);
                }
                self.invoke_with_sim(&mut sim, receiver, callable, args, seed)
            }
        }
    }

    /// Runs the given entry expression on a new instance of the environment and simulator,
    /// but using the current compilation.
    #[allow(clippy::too_many_arguments)]
    pub fn run(
        &mut self,
        receiver: &mut impl Receiver,
        expr: Option<&str>,
        noise: Option<PauliNoise>,
        qubit_loss: Option<f64>,
        noise_config: Option<NoiseConfig<f64, f64>>,
        seed: Option<u64>,
        sim_type: SimType,
    ) -> InterpretResult {
        let qubit_loss = if noise_config.is_none() {
            qubit_loss
        } else {
            None
        };
        match sim_type {
            SimType::Sparse => {
                let mut sim = match noise {
                    Some(noise) => SparseSim::new_with_noise(&noise),
                    None => match noise_config {
                        Some(config) => SparseSim::new_with_noise_config(config.into()),
                        None => SparseSim::new(),
                    },
                };
                if let Some(loss) = qubit_loss {
                    sim.set_loss(loss);
                }
                self.run_with_sim(&mut sim, receiver, expr, seed)
            }
            SimType::Clifford(num_qubits) => {
                let mut sim = match noise_config {
                    Some(config) => CliffordSim::new_with_noise_config(num_qubits, config.into()),
                    None => CliffordSim::new(num_qubits),
                };
                self.run_with_sim(&mut sim, receiver, expr, seed)
            }
        }
    }

    /// Gets the current quantum state of the simulator.
    pub fn get_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize) {
        self.sim
            .capture_quantum_state()
            .expect("interpreter should use infallible sparse sim by default")
    }

    /// Get the current circuit representation of the program.
    pub fn get_circuit(&self) -> Circuit {
        self.circuit_tracer
            .as_ref()
            .expect("to call get_circuit, the interpreter should be initialized with circuit tracing enabled")
            .snapshot(&(self.compiler.package_store(), &self.fir_store))
    }

    /// Performs QIR codegen using the given entry expression on a new instance of the environment
    /// and simulator but using the current compilation.
    pub fn qirgen(&mut self, expr: &str) -> std::result::Result<String, Vec<Error>> {
        if self.capabilities == TargetCapabilityFlags::all() {
            return Err(vec![Error::UnsupportedRuntimeCapabilities]);
        }

        // Compile the expression. This operation will set the expression as
        // the entry-point in the FIR store.
        let (graph, compute_properties) = self.compile_entry_expr(expr)?;

        let Some(compute_properties) = compute_properties else {
            // This can only happen if capability analysis was not run. This would be a bug
            // and we are in a bad state and can't proceed.
            panic!("internal error: compute properties not set after lowering entry expression");
        };
        let package = self.fir_store.get(self.package);
        let entry = ProgramEntry {
            exec_graph: graph,
            expr: (
                self.package,
                package
                    .entry
                    .expect("package must have an entry expression"),
            )
                .into(),
        };
        // Generate QIR
        fir_to_qir(
            &self.fir_store,
            self.capabilities,
            Some(compute_properties),
            &entry,
        )
        .map_err(|e| {
            let hir_package_id = match e.span() {
                Some(span) => span.package,
                None => map_fir_package_to_hir(self.package),
            };
            let source_package = self
                .compiler
                .package_store()
                .get(hir_package_id)
                .expect("package should exist in the package store");
            vec![Error::PartialEvaluation(WithSource::from_map(
                &source_package.sources,
                e,
            ))]
        })
    }

    /// Performs RIR codegen using the given entry expression on a new instance of the environment
    /// and simulator but using the current compilation. Returns the raw and SSA forms of the RIR
    /// as strings.
    pub fn get_rir(&mut self, expr: &str) -> std::result::Result<Vec<String>, Vec<Error>> {
        if self.capabilities == TargetCapabilityFlags::all() {
            return Err(vec![Error::UnsupportedRuntimeCapabilities]);
        }

        // Compile the expression. This operation will set the expression as
        // the entry-point in the FIR store.
        let (graph, compute_properties) = self.compile_entry_expr(expr)?;

        let Some(compute_properties) = compute_properties else {
            // This can only happen if capability analysis was not run. This would be a bug
            // and we are in a bad state and can't proceed.
            panic!("internal error: compute properties not set after lowering entry expression");
        };
        let package = self.fir_store.get(self.package);
        let entry = ProgramEntry {
            exec_graph: graph,
            expr: (
                self.package,
                package
                    .entry
                    .expect("package must have an entry expression"),
            )
                .into(),
        };
        let (raw, ssa) = fir_to_rir(
            &self.fir_store,
            self.capabilities,
            Some(compute_properties),
            &entry,
            PartialEvalConfig {
                generate_debug_metadata: true,
            },
        )
        .map_err(|e| {
            let hir_package_id = match e.span() {
                Some(span) => span.package,
                None => map_fir_package_to_hir(self.package),
            };
            let source_package = self
                .compiler
                .package_store()
                .get(hir_package_id)
                .expect("package should exist in the package store");
            vec![Error::PartialEvaluation(WithSource::from_map(
                &source_package.sources,
                e,
            ))]
        })?;
        Ok(vec![raw.to_string(), ssa.to_string()])
    }

    /// Performs QIR codegen using the given callable with the given arguments on a new instance of the environment
    /// and simulator but using the current compilation.
    pub fn qirgen_from_callable(
        &mut self,
        callable: &Value,
        args: Value,
    ) -> std::result::Result<String, Vec<Error>> {
        if self.capabilities == TargetCapabilityFlags::all() {
            return Err(vec![Error::UnsupportedRuntimeCapabilities]);
        }

        let Value::Global(store_item_id, _) = callable else {
            return Err(vec![Error::NotACallable]);
        };

        fir_to_qir_from_callable(
            &self.fir_store,
            self.capabilities,
            None,
            *store_item_id,
            args,
        )
        .map_err(|e| {
            let hir_package_id = match e.span() {
                Some(span) => span.package,
                None => map_fir_package_to_hir(self.package),
            };
            let source_package = self
                .compiler
                .package_store()
                .get(hir_package_id)
                .expect("package should exist in the package store");
            vec![Error::PartialEvaluation(WithSource::from_map(
                &source_package.sources,
                e,
            ))]
        })
    }

    /// Generates a circuit representation for the program.
    ///
    /// For `entry` options, see [`CircuitEntryPoint`]. For `tracer_config` options, see [`TracerConfig`].
    pub fn circuit(
        &mut self,
        entry: CircuitEntryPoint,
        method: CircuitGenerationMethod,
        tracer_config: TracerConfig,
    ) -> std::result::Result<Circuit, Vec<Error>> {
        let (entry_expr, qubit_params, invoke_params) = match entry {
            CircuitEntryPoint::Operation(operation_expr) => {
                let (package_id, item, functor_app) = self.eval_to_operation(&operation_expr)?;
                let qubit_param_info = qubit_param_info(item);
                let expr = entry_expr_for_qubit_operation(item, functor_app, &operation_expr)
                    .map_err(|e| vec![e.into()])?;
                (Some(expr), qubit_param_info.map(|i| (package_id, i)), None)
            }
            CircuitEntryPoint::EntryExpr(expr) => (Some(expr), None, None),
            CircuitEntryPoint::Callable(call_val, args_val) => {
                (None, None, Some((call_val, args_val)))
            }
            CircuitEntryPoint::EntryPoint => (None, None, None),
        };

        let mut sink = std::io::sink();
        let mut out = GenericReceiver::new(&mut sink);
        let mut tracer = CircuitTracer::with_qubit_input_params(
            tracer_config,
            &[self.package, self.source_package],
            qubit_params,
        );

        // If grouping by scope is enabled, we'll want to execute
        // debug nodes to track block scopes.
        let eval_config = if tracer_config.group_by_scope {
            ExecGraphConfig::Debug
        } else {
            self.eval_config
        };

        match method {
            CircuitGenerationMethod::Simulate => {
                let mut sim = SparseSim::new();
                let mut tracing_backend = TracingBackend::new(&mut sim, Some(&mut tracer));
                if let Some((callable, args)) = invoke_params {
                    self.invoke_with_tracing_backend(
                        &mut tracing_backend,
                        &mut out,
                        callable,
                        args,
                        eval_config,
                        None,
                    )?;
                } else {
                    self.run_with_tracing_backend(
                        &mut tracing_backend,
                        &mut out,
                        entry_expr.as_deref(),
                        eval_config,
                    )?;
                }
            }
            CircuitGenerationMethod::ClassicalEval => {
                let mut tracer = TracingBackend::<SparseSim>::no_backend(&mut tracer);
                if let Some((callable, args)) = invoke_params {
                    self.invoke_with_tracing_backend(
                        &mut tracer,
                        &mut out,
                        callable,
                        args,
                        eval_config,
                        None,
                    )?;
                } else {
                    self.run_with_tracing_backend(
                        &mut tracer,
                        &mut out,
                        entry_expr.as_deref(),
                        eval_config,
                    )?;
                }
            }
            CircuitGenerationMethod::Static => {
                if let Some((callable, args)) = invoke_params {
                    return self.static_circuit_from_callable(&callable, args, tracer_config);
                }
                return self.static_circuit(entry_expr.as_deref(), tracer_config);
            }
        }
        let circuit = tracer.finish(&(self.compiler.package_store(), &self.fir_store));
        Ok(circuit)
    }

    fn static_circuit(
        &mut self,
        entry_expr: Option<&str>,
        tracer_config: TracerConfig,
    ) -> std::result::Result<Circuit, Vec<Error>> {
        if self.capabilities > Profile::AdaptiveRIF.into() {
            return Err(vec![Error::UnsupportedRuntimeCapabilities]);
        }

        let program = self.compile_to_rir_with_debug_metadata(entry_expr)?;
        rir_to_circuit(
            &program,
            tracer_config,
            &[self.package, self.source_package],
            &(self.compiler.package_store(), &self.fir_store),
        )
        .map_err(|e| vec![e.into()])
    }

    fn static_circuit_from_callable(
        &mut self,
        callable: &Value,
        args: Value,
        tracer_config: TracerConfig,
    ) -> std::result::Result<Circuit, Vec<Error>> {
        if self.capabilities > Profile::AdaptiveRIF.into() {
            return Err(vec![Error::UnsupportedRuntimeCapabilities]);
        }

        let Value::Global(store_item_id, _) = callable else {
            return Err(vec![Error::NotACallable]);
        };

        let (_original, transformed) = fir_to_rir_from_callable(
            &self.fir_store,
            self.capabilities,
            None,
            *store_item_id,
            args,
            PartialEvalConfig {
                generate_debug_metadata: true,
            },
        )
        .map_err(|e| {
            let hir_package_id = match e.span() {
                Some(span) => span.package,
                None => map_fir_package_to_hir(self.package),
            };
            let source_package = self
                .compiler
                .package_store()
                .get(hir_package_id)
                .expect("package should exist in the package store");
            vec![Error::PartialEvaluation(WithSource::from_map(
                &source_package.sources,
                e,
            ))]
        })?;

        rir_to_circuit(
            &transformed,
            tracer_config,
            &[self.package, self.source_package],
            &(self.compiler.package_store(), &self.fir_store),
        )
        .map_err(|e| vec![e.into()])
    }

    fn compile_to_rir_with_debug_metadata(
        &mut self,
        entry_expr: Option<&str>,
    ) -> std::result::Result<qsc_partial_eval::Program, Vec<Error>> {
        let (entry, compute_properties) = if let Some(entry_expr) = &entry_expr {
            // Compile the expression. This operation will set the expression as
            // the entry-point in the FIR store.
            let (graph, compute_properties) = self.compile_entry_expr(entry_expr)?;

            let Some(compute_properties) = compute_properties else {
                // This can only happen if capability analysis was not run.
                panic!(
                    "internal error: compute properties not set after lowering entry expression"
                );
            };
            let package = self.fir_store.get(self.package);
            let entry = ProgramEntry {
                exec_graph: graph,
                expr: (
                    self.package,
                    package
                        .entry
                        .expect("package must have an entry expression"),
                )
                    .into(),
            };
            (entry, compute_properties)
        } else {
            let package = self.fir_store.get(self.source_package);
            let entry = ProgramEntry {
                exec_graph: package.entry_exec_graph.clone(),
                expr: (
                    self.source_package,
                    package
                        .entry
                        .expect("package must have an entry expression"),
                )
                    .into(),
            };
            (
                entry,
                self.compute_properties.clone().expect(
                    "compute properties should be set if target profile isn't unrestricted",
                ),
            )
        };
        let (_original, transformed) = fir_to_rir(
            &self.fir_store,
            self.capabilities,
            Some(compute_properties),
            &entry,
            PartialEvalConfig {
                generate_debug_metadata: true,
            },
        )
        .map_err(|e| {
            let hir_package_id = match e.span() {
                Some(span) => span.package,
                None => map_fir_package_to_hir(self.package),
            };
            let source_package = self
                .compiler
                .package_store()
                .get(hir_package_id)
                .expect("package should exist in the package store");
            vec![Error::PartialEvaluation(WithSource::from_map(
                &source_package.sources,
                e,
            ))]
        })?;
        Ok(transformed)
    }

    /// Sets the entry expression for the interpreter.
    pub fn set_entry_expr(&mut self, entry_expr: &str) -> std::result::Result<(), Vec<Error>> {
        let (graph, _) = self.compile_entry_expr(entry_expr)?;
        self.expr_graph = Some(graph);
        Ok(())
    }

    /// Runs the given entry expression on the given simulator with a new instance of the environment
    /// but using the current compilation.
    pub fn run_with_sim(
        &mut self,
        sim: &mut impl Backend,
        receiver: &mut impl Receiver,
        expr: Option<&str>,
        seed: Option<u64>,
    ) -> InterpretResult {
        let mut tracing_backend = TracingBackend::no_tracer(sim);
        let graph = if let Some(expr) = expr {
            let (graph, _) = self.compile_entry_expr(expr)?;
            self.expr_graph = Some(graph.clone());
            graph
        } else {
            self.expr_graph.clone().ok_or(vec![Error::NoEntryPoint])?
        };

        if seed.is_some() {
            tracing_backend.set_seed(seed);
        } else if self.quantum_seed.is_some() {
            tracing_backend.set_seed(self.quantum_seed);
        }

        let classical_seed = match seed {
            Some(seed) => Some(seed),
            None => self.classical_seed,
        };

        eval(
            self.package,
            classical_seed,
            graph,
            self.eval_config,
            self.compiler.package_store(),
            &self.fir_store,
            &mut Env::default(),
            &mut tracing_backend,
            receiver,
        )
    }

    fn run_with_tracing_backend<B: Backend>(
        &mut self,
        tracing_backend: &mut TracingBackend<'_, B>,
        out: &mut GenericReceiver,
        entry_expr: Option<&str>,
        config: ExecGraphConfig,
    ) -> InterpretResult {
        let (package_id, graph) = if let Some(entry_expr) = entry_expr {
            // entry expression is provided
            let (graph, _) = self.compile_entry_expr(entry_expr)?;
            (self.package, graph)
        } else {
            // no entry expression, use the entrypoint in the package
            (self.source_package, self.get_entry_exec_graph()?)
        };
        if self.quantum_seed.is_some() {
            tracing_backend.set_seed(self.quantum_seed);
        }
        eval(
            package_id,
            self.classical_seed,
            graph,
            config,
            self.compiler.package_store(),
            &self.fir_store,
            &mut Env::default(),
            tracing_backend,
            out,
        )
    }

    /// Invokes the given callable with the given arguments on the given simulator with a new instance of the environment
    /// but using the current compilation.
    pub fn invoke_with_sim(
        &mut self,
        sim: &mut impl Backend,
        receiver: &mut impl Receiver,
        callable: Value,
        args: Value,
        seed: Option<u64>,
    ) -> InterpretResult {
        self.invoke_with_tracing_backend(
            &mut TracingBackend::no_tracer(sim),
            receiver,
            callable,
            args,
            self.eval_config,
            seed,
        )
    }

    fn invoke_with_tracing_backend<B: Backend>(
        &mut self,
        tracing_backend: &mut TracingBackend<'_, B>,
        receiver: &mut impl Receiver,
        callable: Value,
        args: Value,
        config: ExecGraphConfig,
        seed: Option<u64>,
    ) -> InterpretResult {
        let classical_seed = match seed {
            Some(seed) => Some(seed),
            None => self.classical_seed,
        };
        qsc_eval::invoke(
            self.package,
            classical_seed,
            &self.fir_store,
            config,
            &mut Env::default(),
            tracing_backend,
            receiver,
            callable,
            args,
        )
        .map_err(|(error, call_stack)| {
            eval_error(
                self.compiler.package_store(),
                &self.fir_store,
                call_stack,
                error,
            )
        })
    }

    fn compile_entry_expr(
        &mut self,
        expr: &str,
    ) -> std::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
        let increment = self
            .compiler
            .compile_entry_expr(expr)
            .map_err(into_errors)?;

        // `lower` will update the entry expression in the FIR store,
        // and it will always return an empty list of statements.
        let (graph, compute_properties) = self.lower(&increment)?;

        // The AST and HIR packages in `increment` only contain an entry
        // expression and no statements. The HIR *can* contain items if the entry
        // expression defined any items.
        assert!(increment.hir.stmts.is_empty());
        assert!(increment.ast.package.nodes.is_empty());

        // Updating the compiler state with the new AST/HIR nodes
        // is not necessary for the interpreter to function, as all
        // the state required for evaluation already exists in the
        // FIR store. It could potentially save some memory
        // *not* to do hold on to the AST/HIR, but it is done
        // here to keep the package stores consistent.
        self.compiler.update(increment);

        Ok((graph, compute_properties))
    }

    fn lower(
        &mut self,
        unit_addition: &qsc_frontend::incremental::Increment,
    ) -> core::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
        if self.capabilities != TargetCapabilityFlags::all() {
            return self.run_fir_passes(unit_addition);
        }

        self.lower_and_update_package(unit_addition);
        Ok((self.lowerer.take_exec_graph(), None))
    }

    fn lower_and_update_package(&mut self, unit: &qsc_frontend::incremental::Increment) {
        {
            let fir_package = self.fir_store.get_mut(self.package);
            self.lowerer
                .lower_and_update_package(fir_package, &unit.hir);
        }
        let fir_package: &Package = self.fir_store.get(self.package);
        qsc_fir::validate::validate(fir_package, &self.fir_store);
    }

    fn run_fir_passes(
        &mut self,
        unit: &qsc_frontend::incremental::Increment,
    ) -> std::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
        self.lower_and_update_package(unit);

        let cap_results =
            PassContext::run_fir_passes_on_fir(&self.fir_store, self.package, self.capabilities);

        let compute_properties = cap_results.map_err(|caps_errors| {
            // if there are errors, convert them to interpreter errors
            // and revert the update to the lowerer/FIR store.
            let fir_package = self.fir_store.get_mut(self.package);
            self.lowerer.revert_last_increment(fir_package);

            let source_package = self
                .compiler
                .package_store()
                .get(map_fir_package_to_hir(self.package))
                .expect("package should exist in the package store");

            caps_errors
                .into_iter()
                .map(|error| Error::Pass(WithSource::from_map(&source_package.sources, error)))
                .collect::<Vec<_>>()
        })?;

        let graph = self.lowerer.take_exec_graph();
        Ok((graph, Some(compute_properties)))
    }

    fn next_line_label(&mut self) -> String {
        let label = format!("line_{}", self.lines);
        self.lines += 1;
        label
    }

    /// Evaluate the name of an operation, or any expression that evaluates to a callable,
    /// and return the Item ID and function application for the callable.
    /// Examples: "Microsoft.Quantum.Diagnostics.DumpMachine", "(qs: Qubit[]) => H(qs[0])",
    /// "Controlled SWAP"
    fn eval_to_operation(
        &mut self,
        operation_expr: &str,
    ) -> std::result::Result<(PackageId, &qsc_hir::hir::Item, FunctorApp), Vec<Error>> {
        let mut sink = std::io::sink();
        let mut out = GenericReceiver::new(&mut sink);
        let (store_item_id, functor_app) = match self.eval_fragments(&mut out, operation_expr)? {
            Value::Closure(b) => (b.id, b.functor),
            Value::Global(item_id, functor_app) => (item_id, functor_app),
            _ => return Err(vec![Error::NotAnOperation]),
        };
        let package = map_fir_package_to_hir(store_item_id.package);
        let local_item_id = crate::hir::LocalItemId::from(usize::from(store_item_id.item));
        let unit = self
            .compiler
            .package_store()
            .get(package)
            .expect("package should exist in the package store");
        let item = unit
            .package
            .items
            .get(local_item_id)
            .expect("item should exist in the package");
        Ok((store_item_id.package, item, functor_app))
    }
}

#[derive(Debug, Clone)]
/// Describes the entry point for circuit generation.
pub enum CircuitEntryPoint {
    /// An operation. This must be a callable name or a lambda
    /// expression that only takes qubits as arguments.
    /// e.g. "Sample.Main" , "qs => H(qs[0])"
    /// The callable name must be visible in the current package.
    Operation(String),
    /// An explicitly provided entry expression.
    EntryExpr(String),
    /// A global callable with arguments.
    Callable(Value, Value),
    /// The entry point for the current package.
    EntryPoint,
}

/// How the circuit is generated.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CircuitGenerationMethod {
    /// Simulate the program and trace the actual gate calls. Nondeterministic.
    Simulate,
    /// Evaluate the classical parts of the program. No quantum simulation.
    /// Will fail if a measurement comparison occurs during evaluation.
    ClassicalEval,
    /// Compile the program and transform to a circuit with only partial evaluation.
    /// Only works for `AdaptiveRIF` compliant programs.
    Static,
}

/// A debugger that enables step-by-step evaluation of code
/// and inspecting state in the interpreter.
pub struct Debugger {
    interpreter: Interpreter,
    /// The encoding (utf-8 or utf-16) used for character offsets
    /// in line/character positions returned by the Interpreter.
    position_encoding: Encoding,
    /// The current state of the evaluator.
    state: State,
}

impl Debugger {
    pub fn new(
        sources: SourceMap,
        capabilities: TargetCapabilityFlags,
        position_encoding: Encoding,
        language_features: LanguageFeatures,
        store: PackageStore,
        dependencies: &Dependencies,
    ) -> std::result::Result<Self, Vec<Error>> {
        let interpreter = Interpreter::with_debug(
            sources,
            PackageType::Exe,
            capabilities,
            language_features,
            store,
            dependencies,
            Debugger::circuit_config(),
        )?;
        let source_package_id = interpreter.source_package;
        let unit = interpreter.fir_store.get(source_package_id);
        let entry_exec_graph = unit.entry_exec_graph.clone();
        Ok(Self {
            interpreter,
            position_encoding,
            state: State::new(
                source_package_id,
                entry_exec_graph,
                ExecGraphConfig::Debug,
                None,
                ErrorBehavior::StopOnError,
            ),
        })
    }

    pub fn from(interpreter: Interpreter, position_encoding: Encoding) -> Self {
        let source_package_id = interpreter.source_package;
        let unit = interpreter.fir_store.get(source_package_id);
        let entry_exec_graph = unit.entry_exec_graph.clone();
        Self {
            interpreter,
            position_encoding,
            state: State::new(
                source_package_id,
                entry_exec_graph,
                ExecGraphConfig::Debug,
                None,
                ErrorBehavior::StopOnError,
            ),
        }
    }

    /// Resumes execution with specified `StepAction`.
    /// # Errors
    /// Returns a vector of errors if evaluating the entry point fails.
    pub fn eval_step(
        &mut self,
        receiver: &mut impl Receiver,
        breakpoints: &[StmtId],
        step: StepAction,
    ) -> std::result::Result<StepResult, Vec<Error>> {
        self.state
            .eval(
                &self.interpreter.fir_store,
                &mut self.interpreter.env,
                &mut TracingBackend::new(
                    &mut self.interpreter.sim,
                    self.interpreter.circuit_tracer.as_mut(),
                ),
                receiver,
                breakpoints,
                step,
            )
            .map_err(|(error, call_stack)| {
                eval_error(
                    self.interpreter.compiler.package_store(),
                    &self.interpreter.fir_store,
                    call_stack,
                    error,
                )
            })
    }

    #[must_use]
    pub fn get_stack_frames(&self) -> Vec<StackFrame> {
        let frames = self.state.capture_stack();

        frames
            .iter()
            .map(|frame| {
                let callable = self
                    .interpreter
                    .fir_store
                    .get_global(frame.id)
                    .expect("frame should exist");
                let functor = format!("{}", frame.functor);
                let name = match callable {
                    Global::Callable(decl) => decl.name.name.to_string(),
                    Global::Udt => "udt".into(),
                };

                StackFrame {
                    name,
                    functor,
                    location: Location::from(
                        frame.span,
                        map_fir_package_to_hir(frame.id.package),
                        self.interpreter.compiler.package_store(),
                        self.position_encoding,
                    ),
                }
            })
            .collect()
    }

    pub fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize) {
        self.interpreter.get_quantum_state()
    }

    pub fn circuit(&self) -> Circuit {
        self.interpreter.get_circuit()
    }

    #[must_use]
    pub fn get_breakpoints(&self, path: &str) -> Vec<BreakpointSpan> {
        let unit = self.source_package();

        if let Some(source) = unit.sources.find_by_name(path) {
            let package = self
                .interpreter
                .fir_store
                .get(self.interpreter.source_package);
            let mut collector = BreakpointCollector::new(
                &unit.sources,
                source.offset,
                package,
                self.position_encoding,
            );
            collector.visit_package(package, &self.interpreter.fir_store);
            let mut spans: Vec<_> = collector.statements.into_iter().collect();

            // Sort by start position (line first, column next)
            spans.sort_by_key(|s| (s.range.start.line, s.range.start.column));
            spans
        } else {
            Vec::new()
        }
    }

    #[must_use]
    pub fn get_locals(&self, frame_id: usize) -> Vec<VariableInfo> {
        self.interpreter
            .env
            .get_variables_in_frame(frame_id)
            .into_iter()
            .filter(|v| !v.name.starts_with('@'))
            .collect()
    }

    fn source_package(&self) -> &CompileUnit {
        self.interpreter
            .compiler
            .package_store()
            .get(map_fir_package_to_hir(self.interpreter.source_package))
            .expect("Could not load package")
    }

    /// Configuration used to trace the circuit while debugging.
    fn circuit_config() -> TracerConfig {
        TracerConfig {
            max_operations: TracerConfig::DEFAULT_MAX_OPERATIONS,
            source_locations: true,
            group_by_scope: false,
            prune_classical_qubits: false,
        }
    }
}

/// Wrapper function for `qsc_eval::eval` that handles error conversion.
#[allow(clippy::too_many_arguments)]
fn eval<B: Backend>(
    package: PackageId,
    classical_seed: Option<u64>,
    exec_graph: ExecGraph,
    exec_graph_config: ExecGraphConfig,
    package_store: &PackageStore,
    fir_store: &fir::PackageStore,
    env: &mut Env,
    tracing_backend: &mut TracingBackend<'_, B>,
    receiver: &mut impl Receiver,
) -> InterpretResult {
    qsc_eval::eval(
        package,
        classical_seed,
        exec_graph,
        exec_graph_config,
        fir_store,
        env,
        tracing_backend,
        receiver,
    )
    .map_err(|(error, call_stack)| eval_error(package_store, fir_store, call_stack, error))
}

/// Represents a stack frame for debugging.
pub struct StackFrame {
    /// The name of the callable.
    pub name: String,
    /// The functor of the callable.
    pub functor: String,
    /// The source location of the call site.
    pub location: Location,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct BreakpointSpan {
    /// The id of the statement representing the breakpoint location.
    pub id: u32,
    /// The source range of the call site.
    pub range: Range,
}

struct BreakpointCollector<'a> {
    statements: FxHashSet<BreakpointSpan>,
    sources: &'a SourceMap,
    offset: u32,
    package: &'a Package,
    position_encoding: Encoding,
}

impl<'a> BreakpointCollector<'a> {
    fn new(
        sources: &'a SourceMap,
        offset: u32,
        package: &'a Package,
        position_encoding: Encoding,
    ) -> Self {
        Self {
            statements: FxHashSet::default(),
            sources,
            offset,
            package,
            position_encoding,
        }
    }

    fn get_source(&self, offset: u32) -> &Source {
        self.sources
            .find_by_offset(offset)
            .expect("Couldn't find source file")
    }

    fn add_stmt(&mut self, stmt: &fir::Stmt) {
        let source: &Source = self.get_source(stmt.span.lo);
        if source.offset == self.offset {
            let span = stmt.span - source.offset;
            if span != Span::default() {
                let bps = BreakpointSpan {
                    id: stmt.id.into(),
                    range: Range::from_span(self.position_encoding, &source.contents, &span),
                };
                self.statements.insert(bps);
            }
        }
    }
}

impl<'a> Visitor<'a> for BreakpointCollector<'a> {
    fn visit_stmt(&mut self, stmt: StmtId) {
        let stmt_res = self.get_stmt(stmt);
        match stmt_res.kind {
            fir::StmtKind::Expr(expr) | fir::StmtKind::Local(_, _, expr) => {
                self.add_stmt(stmt_res);
                visit::walk_expr(self, expr);
            }
            fir::StmtKind::Item(_) | fir::StmtKind::Semi(_) => {
                self.add_stmt(stmt_res);
            }
        }
    }

    fn get_block(&self, id: BlockId) -> &'a Block {
        self.package
            .blocks
            .get(id)
            .expect("couldn't find block in FIR")
    }

    fn get_expr(&self, id: ExprId) -> &'a Expr {
        self.package
            .exprs
            .get(id)
            .expect("couldn't find expr in FIR")
    }

    fn get_pat(&self, id: PatId) -> &'a Pat {
        self.package.pats.get(id).expect("couldn't find pat in FIR")
    }

    fn get_stmt(&self, id: StmtId) -> &'a Stmt {
        self.package
            .stmts
            .get(id)
            .expect("couldn't find stmt in FIR")
    }
}

fn eval_error(
    package_store: &PackageStore,
    fir_store: &fir::PackageStore,
    call_stack: Vec<Frame>,
    error: qsc_eval::Error,
) -> Vec<Error> {
    let stack_trace = if call_stack.is_empty() {
        None
    } else {
        Some(format_call_stack(
            package_store,
            fir_store,
            call_stack,
            &error,
        ))
    };

    vec![error::from_eval(error, package_store, stack_trace).into()]
}

#[must_use]
pub fn into_errors(errors: Vec<crate::compile::Error>) -> Vec<Error> {
    errors
        .into_iter()
        .map(|error| Error::Compile(error.into_with_source()))
        .collect::<Vec<_>>()
}
