// © 2023, The RefinedRust Developers and Contributors
//
// This Source Code Form is subject to the terms of the BSD-3-clause License.
// If a copy of the BSD-3-clause license was not distributed with this
// file, You can obtain one at https://opensource.org/license/bsd-3-clause/.

use std::collections::{btree_map, BTreeMap, HashMap, HashSet};

use log::{info, trace, warn};
use radium::coq;
use rr_rustc_interface::hir::def_id::DefId;
use rr_rustc_interface::middle::mir::interpret::{ConstValue, ErrorHandled, Scalar};
use rr_rustc_interface::middle::mir::tcx::PlaceTy;
use rr_rustc_interface::middle::mir::{
    BasicBlock, BasicBlockData, BinOp, Body, BorrowKind, Constant, ConstantKind, Local, LocalKind, Location,
    Mutability, Operand, Place, ProjectionElem, Rvalue, StatementKind, Terminator, TerminatorKind, UnOp,
    VarDebugInfoContents,
};
use rr_rustc_interface::middle::ty::fold::TypeFolder;
use rr_rustc_interface::middle::ty::{ConstKind, Ty, TyKind, TypeFoldable};
use rr_rustc_interface::middle::{mir, ty};
use rr_rustc_interface::{abi, ast, middle};

use crate::arg_folder::*;
use crate::base::*;
use crate::checked_op_analysis::CheckedOpLocalAnalysis;
use crate::environment::borrowck::facts;
use crate::environment::polonius_info::PoloniusInfo;
use crate::environment::procedure::Procedure;
use crate::environment::{dump_borrowck_info, polonius_info, Environment};
use crate::inclusion_tracker::*;
use crate::spec_parsers::verbose_function_spec_parser::{
    ClosureMetaInfo, FunctionSpecParser, VerboseFunctionSpecParser,
};
use crate::type_translator::*;
use crate::tyvars::*;
use crate::{traits, utils};

/**
 * Tracks the functions we translated and the Coq names they are available under.
 * To account for dependencies between functions, we may register translated names before we have
 * actually translated the function.
 */

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ProcedureMode {
    Prove,
    OnlySpec,
    TrustMe,
    Shim,
    Ignore,
}

impl ProcedureMode {
    pub fn is_prove(self) -> bool {
        self == Self::Prove
    }

    pub fn is_only_spec(self) -> bool {
        self == Self::OnlySpec
    }

    pub fn is_trust_me(self) -> bool {
        self == Self::TrustMe
    }

    pub fn is_shim(self) -> bool {
        self == Self::Shim
    }

    pub fn is_ignore(self) -> bool {
        self == Self::Ignore
    }

    pub fn needs_proof(self) -> bool {
        self == Self::Prove
    }

    pub fn needs_def(self) -> bool {
        self == Self::Prove || self == Self::TrustMe
    }
}

#[derive(Clone)]
pub struct ProcedureMeta {
    spec_name: String,
    name: String,
    mode: ProcedureMode,
}

impl ProcedureMeta {
    pub const fn new(spec_name: String, name: String, mode: ProcedureMode) -> Self {
        Self {
            spec_name,
            name,
            mode,
        }
    }

    pub fn get_spec_name(&self) -> &str {
        &self.spec_name
    }

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

    pub const fn get_mode(&self) -> ProcedureMode {
        self.mode
    }
}

pub struct ProcedureScope<'def> {
    /// maps the defid to (code_name, spec_name, name)
    name_map: BTreeMap<DefId, ProcedureMeta>,
    /// track the actually translated functions
    translated_functions: BTreeMap<DefId, radium::Function<'def>>,
    /// track the functions with just a specification (rr::only_spec)
    specced_functions: BTreeMap<DefId, radium::FunctionSpec<'def>>,
}

impl<'def> ProcedureScope<'def> {
    pub fn new() -> Self {
        Self {
            name_map: BTreeMap::new(),
            translated_functions: BTreeMap::new(),
            specced_functions: BTreeMap::new(),
        }
    }

    pub fn lookup_function(&self, did: DefId) -> Option<ProcedureMeta> {
        self.name_map.get(&did).cloned()
    }

    /// Lookup the Coq spec name for a function.
    pub fn lookup_function_spec_name(&self, did: DefId) -> Option<&str> {
        self.name_map.get(&did).map(ProcedureMeta::get_spec_name)
    }

    /// Lookup the name for a function.
    pub fn lookup_function_mangled_name(&self, did: DefId) -> Option<&str> {
        self.name_map.get(&did).map(ProcedureMeta::get_name)
    }

    /// Lookup the mode for a function.
    pub fn lookup_function_mode(&self, did: DefId) -> Option<ProcedureMode> {
        self.name_map.get(&did).map(ProcedureMeta::get_mode)
    }

    /// Register a function.
    pub fn register_function(&mut self, did: DefId, meta: ProcedureMeta) -> Result<(), String> {
        if self.name_map.insert(did, meta).is_some() {
            Err(format!("function for defid {:?} has already been registered", did))
        } else {
            Ok(())
        }
    }

    /// Provide the code for a translated function.
    pub fn provide_translated_function(&mut self, did: DefId, trf: radium::Function<'def>) {
        let meta = &self.name_map[&did];
        assert!(meta.get_mode().needs_def());
        assert!(self.translated_functions.insert(did, trf).is_none());
    }

    /// Provide the specification for an `only_spec` function.
    pub fn provide_specced_function(&mut self, did: DefId, spec: radium::FunctionSpec<'def>) {
        let meta = &self.name_map[&did];
        assert!(meta.get_mode().is_only_spec());
        assert!(self.specced_functions.insert(did, spec).is_none());
    }

    /// Iterate over the functions we have generated code for.
    pub fn iter_code(&self) -> btree_map::Iter<'_, DefId, radium::Function<'def>> {
        self.translated_functions.iter()
    }

    /// Iterate over the functions we have generated only specs for.
    pub fn iter_only_spec(&self) -> btree_map::Iter<'_, DefId, radium::FunctionSpec<'def>> {
        self.specced_functions.iter()
    }
}

/// Scope of consts that are available
pub struct ConstScope<'def> {
    pub statics: HashMap<DefId, radium::StaticMeta<'def>>,
}

// solve the constraints for the new_regions
// we first identify what kinds of constraints these new regions are subject to
#[derive(Debug)]
enum CallRegionKind {
    // this is just an intersection of local regions.
    Intersection(HashSet<Region>),
    // this is equal to a specific region
    EqR(Region),
}

struct CallRegions {
    pub early_regions: Vec<Region>,
    pub late_regions: Vec<Region>,
    pub classification: HashMap<Region, CallRegionKind>,
}

pub struct FunctionTranslator<'a, 'def, 'tcx> {
    env: &'def Environment<'tcx>,
    /// this needs to be annotated with the right borrowck things
    proc: &'def Procedure<'tcx>,
    /// the Caesium function buildder
    translated_fn: radium::FunctionBuilder<'def>,
    /// tracking lifetime inclusions for the generation of lifetime inclusions
    inclusion_tracker: InclusionTracker<'a, 'tcx>,

    /// registry of other procedures
    procedure_registry: &'a ProcedureScope<'def>,
    /// registry of consts
    const_registry: &'a ConstScope<'def>,
    /// attributes on this function
    attrs: &'a [&'a ast::ast::AttrItem],
    /// polonius info for this function
    info: &'a PoloniusInfo<'a, 'tcx>,
    /// translator for types
    ty_translator: LocalTypeTranslator<'a, 'def, 'tcx>,
    /// argument types (from the signature, with generics substituted)
    inputs: Vec<Ty<'tcx>>,
}

impl<'a, 'def: 'a, 'tcx: 'def> FunctionTranslator<'a, 'def, 'tcx> {
    pub fn process_lifetimes_of_args(
        env: &Environment<'tcx>,
        params: &[ty::GenericArg<'tcx>],
        sig: ty::Binder<'tcx, ty::FnSig<'tcx>>,
        _body: &mir::Body<'tcx>,
    ) -> (Vec<ty::Ty<'tcx>>, ty::Ty<'tcx>, HashMap<ty::RegionVid, (String, Option<String>)>) {
        let mut universal_lifetimes = HashMap::new();

        // we create a substitution that replaces early bound regions with their Polonius
        // region variables
        let mut subst_early_bounds: Vec<ty::GenericArg<'tcx>> = Vec::new();
        let mut num_early_bounds = 0;
        for a in params {
            match a.unpack() {
                ty::GenericArgKind::Lifetime(r) => {
                    // skip over 0 = static
                    let next_id = ty::RegionVid::from_u32(num_early_bounds + 1);
                    let revar = ty::Region::new_var(env.tcx(), next_id);
                    num_early_bounds += 1;
                    subst_early_bounds.push(ty::GenericArg::from(revar));

                    match *r {
                        ty::RegionKind::ReEarlyBound(r) => {
                            let name = strip_coq_ident(r.name.as_str());
                            universal_lifetimes.insert(next_id, (format!("ulft_{}", name), Some(name)));
                        },
                        _ => {
                            universal_lifetimes.insert(next_id, (format!("ulft_{}", num_early_bounds), None));
                        },
                    }
                },
                _ => {
                    subst_early_bounds.push(*a);
                },
            }
        }
        let subst_early_bounds = env.tcx().mk_args(&subst_early_bounds);

        // add names for late bound region variables
        let mut num_late_bounds = 0;
        for b in sig.bound_vars() {
            let next_id = ty::RegionVid::from_u32(num_early_bounds + num_late_bounds + 1);

            let ty::BoundVariableKind::Region(r) = b else {
                continue;
            };

            match r {
                ty::BoundRegionKind::BrNamed(_, sym) => {
                    let mut region_name = strip_coq_ident(sym.as_str());
                    if region_name == "_" {
                        region_name = next_id.as_usize().to_string();
                        universal_lifetimes.insert(next_id, (format!("ulft_{}", region_name), None));
                    } else {
                        universal_lifetimes
                            .insert(next_id, (format!("ulft_{}", region_name), Some(region_name)));
                    }
                },
                ty::BoundRegionKind::BrAnon(_) => {
                    universal_lifetimes.insert(next_id, (format!("ulft_{}", next_id.as_usize()), None));
                },
                _ => (),
            }

            num_late_bounds += 1;
        }

        // replace late-bound region variables by re-enumerating them in the same way as the MIR
        // type checker does (that this happens in the same way is important to make the names
        // line up!)
        let mut next_index = num_early_bounds + 1; // skip over one additional due to static
        let mut folder = |_| {
            let cur_index = next_index;
            next_index += 1;
            ty::Region::new_var(env.tcx(), ty::RegionVid::from_u32(cur_index))
        };
        let (late_sig, _late_region_map) = env.tcx().replace_late_bound_regions(sig, &mut folder);

        // replace early bound variables
        let inputs: Vec<_> = late_sig
            .inputs()
            .iter()
            .map(|ty| ty_instantiate(*ty, env.tcx(), subst_early_bounds))
            .collect();

        let output = ty_instantiate(late_sig.output(), env.tcx(), subst_early_bounds);

        (inputs, output, universal_lifetimes)
    }

    /// At the start of the function, there's a universal (placeholder) region for reference argument.
    /// These get subsequently relabeled.
    /// Given the relabeled region, find the original placeholder region.
    fn find_placeholder_region_for(r: ty::RegionVid, info: &PoloniusInfo) -> Option<ty::RegionVid> {
        let root_location = Location {
            block: BasicBlock::from_u32(0),
            statement_index: 0,
        };
        let root_point = info.interner.get_point_index(&facts::Point {
            location: root_location,
            typ: facts::PointType::Start,
        });

        for (r1, r2, p) in &info.borrowck_in_facts.subset_base {
            if *p == root_point && *r2 == r {
                info!("find placeholder region for: {:?} ⊑ {:?} = r = {:?}", r1, r2, r);
                return Some(*r1);
            }
        }
        None
    }

    /// Create a translation instance for a closure.
    pub fn new_closure(
        env: &'def Environment<'tcx>,
        meta: &ProcedureMeta,
        proc: Procedure<'tcx>,
        attrs: &'a [&'a ast::ast::AttrItem],
        ty_translator: &'def TypeTranslator<'def, 'tcx>,
        proc_registry: &'a ProcedureScope<'def>,
        const_registry: &'a ConstScope<'def>,
    ) -> Result<Self, TranslationError> {
        let mut translated_fn = radium::FunctionBuilder::new(&meta.name, &meta.spec_name);

        // TODO can we avoid the leak
        let proc: &'def Procedure = &*Box::leak(Box::new(proc));
        let body = proc.get_mir();
        Self::dump_body(body);

        let ty: ty::EarlyBinder<Ty<'tcx>> = env.tcx().type_of(proc.get_id());
        let ty = ty.instantiate_identity();

        let closure_kind = match ty.kind() {
            TyKind::Closure(_def, closure_args) => {
                assert!(ty.is_closure());
                closure_args.as_closure().kind()
            },
            _ => panic!("can not handle non-closures"),
        };

        let local_decls = &body.local_decls;
        let closure_arg = local_decls.get(Local::from_usize(1)).unwrap();

        let closure_ty = match closure_kind {
            ty::ClosureKind::Fn => {
                let ty::TyKind::Ref(_, ty, _) = closure_arg.ty.kind() else {
                    unreachable!();
                };

                ty
            },

            ty::ClosureKind::FnMut => {
                let ty::TyKind::Ref(_, ty, _) = closure_arg.ty.kind() else {
                    unreachable!("unexpected type {:?}", closure_arg.ty);
                };

                ty
            },

            ty::ClosureKind::FnOnce => &closure_arg.ty,
        };

        let mut capture_regions = Vec::new();

        let ty::TyKind::Closure(did, closure_args) = closure_ty.kind() else {
            unreachable!();
        };

        let clos = closure_args.as_closure();

        let tupled_upvars_tys = clos.tupled_upvars_ty();
        let upvars_tys = clos.upvar_tys();
        let parent_args = clos.parent_args();
        let unnormalized_sig = clos.sig();

        let sig = normalize_in_function(proc.get_id(), env.tcx(), unnormalized_sig)?;
        info!("closure sig: {:?}", sig);

        let captures = env.tcx().closure_captures(did.as_local().unwrap());
        info!("Closure has captures: {:?}", captures);

        // find additional lifetime parameters
        for (place, ty) in captures.iter().zip(clos.upvar_tys().iter()) {
            if place.region.is_some() {
                // find region from ty
                if let ty::TyKind::Ref(region, _, _) = ty.kind() {
                    capture_regions.push(*region);
                }
            }
        }

        info!("Closure capture regions: {:?}", capture_regions);
        info!("Closure arg upvar_tys: {:?}", tupled_upvars_tys);
        info!("Function signature: {:?}", sig);
        info!("Closure generic args: {:?}", parent_args);

        let info = match PoloniusInfo::new(env, proc) {
            Ok(info) => info,
            Err(err) => return Err(TranslationError::UnknownError(format!("{:?}", err))),
        };

        // TODO: avoid leak
        let info: &'def PoloniusInfo = &*Box::leak(Box::new(info));

        // For closures, we only handle the parent's args here!
        // TODO: do we need to do something special for the parent's late-bound region
        // parameters?
        // TODO: should we always take the lifetime parameters?
        let params = parent_args;
        //proc.get_type_params();
        info!("Function generic args: {:?}", params);

        // dump graphviz files
        // color code: red: dying loan, pink: becoming a zombie; green: is zombie
        if rrconfig::dump_borrowck_info() {
            dump_borrowck_info(env, proc.get_id(), info);
        }

        let (tupled_inputs, output, mut universal_lifetimes) =
            Self::process_lifetimes_of_args(env, params, sig, body);

        // Process the lifetime parameters that come from the captures
        for r in capture_regions {
            // TODO: problem: we're introducing inconsistent names here.
            let ty::RegionKind::ReVar(r) = r.kind() else {
                unreachable!();
            };

            let lft = info.mk_atomic_region(r);
            let name = format_atomic_region_direct(&lft, None);
            universal_lifetimes.insert(r, (name, None));
        }

        // also add the lifetime for the outer reference
        let mut maybe_outer_lifetime = None;
        if let ty::TyKind::Ref(r, _, _) = closure_arg.ty.kind() {
            let ty::RegionKind::ReVar(r) = r.kind() else {
                unreachable!();
            };

            // We need to do some hacks here to find the right Polonius region:
            // `r` is the non-placeholder region that the variable gets, but we are
            // looking for the corresponding placeholder region
            let r2 = Self::find_placeholder_region_for(r, info).unwrap();

            info!("using lifetime {:?} for closure universal", r2);
            let lft = info.mk_atomic_region(r2);
            let name = format_atomic_region_direct(&lft, None);
            universal_lifetimes.insert(r2, (name, None));

            maybe_outer_lifetime = Some(r2);
        }

        // detuple the inputs
        assert!(tupled_inputs.len() == 1);
        let input_tuple_ty = tupled_inputs[0];
        let mut inputs = Vec::new();

        // push the closure as the first argument
        /*
        if let Some(r2) = maybe_outer_lifetime {
            // in this case, we need to patch the region first
            if let ty::TyKind::Ref(_, ty, m) = closure_arg.ty.kind() {
                let new_region = ty::Region::new_var(env.tcx(), r2);
                inputs.push(env.tcx().mk_ty_from_kind(ty::TyKind::Ref(new_region, *ty, *m)));
            }
        }
        else {
            inputs.push(closure_arg.ty);
        }
        */

        if let ty::TyKind::Tuple(args) = input_tuple_ty.kind() {
            inputs.extend(args.iter());
        }

        info!("inputs({}): {:?}, output: {:?}", inputs.len(), inputs, output);
        info!("Have lifetime parameters: {:?}", universal_lifetimes);

        // add universal lifetimes to the spec
        for (lft, name) in universal_lifetimes.values() {
            //let lft = info::AtomicRegion::Universal(info::UniversalRegionKind::Local,
            // ty::RegionVid::from_u32(1+r));
            translated_fn
                .add_universal_lifetime(name.clone(), lft.to_string())
                .map_err(TranslationError::UnknownError)?;
        }

        let mut inclusion_tracker = InclusionTracker::new(info);
        // add placeholder subsets
        let initial_point: facts::Point = facts::Point {
            location: BasicBlock::from_u32(0).start_location(),
            typ: facts::PointType::Start,
        };
        for (r1, r2) in &info.borrowck_in_facts.known_placeholder_subset {
            inclusion_tracker.add_static_inclusion(*r1, *r2, info.interner.get_point_index(&initial_point));
        }

        // enter the procedure
        let universal_lifetime_map: HashMap<_, _> =
            universal_lifetimes.into_iter().map(|(x, y)| (x, y.0)).collect();
        let type_scope =
            TypeTranslationScope::new(proc.get_id(), env.tcx().mk_args(params), universal_lifetime_map)?;
        // add generic args to the fn
        let generics = &type_scope.generic_scope;
        for t in generics.iter().flatten() {
            translated_fn.add_generic_type(t.clone());
        }

        let mut t = Self {
            env,
            proc,
            info,
            translated_fn,
            inclusion_tracker,
            procedure_registry: proc_registry,
            attrs,
            ty_translator: LocalTypeTranslator::new(ty_translator, type_scope),
            const_registry,
            inputs: inputs.clone(),
        };

        // add universal constraints
        let universal_constraints = t.get_relevant_universal_constraints();
        info!("univeral constraints: {:?}", universal_constraints);
        for (lft1, lft2) in universal_constraints {
            t.translated_fn
                .add_universal_lifetime_constraint(lft1, lft2)
                .map_err(TranslationError::UnknownError)?;
        }

        // compute meta information needed to generate the spec
        let mut translated_upvars_types = Vec::new();
        for ty in upvars_tys {
            let translated_ty = t.ty_translator.translate_type(ty)?;
            translated_upvars_types.push(translated_ty);
        }

        let meta = {
            let scope = t.ty_translator.scope.borrow();

            ClosureMetaInfo {
                kind: closure_kind,
                captures,
                capture_tys: &translated_upvars_types,
                closure_lifetime: maybe_outer_lifetime.map(|x| scope.lookup_universal_region(x).unwrap()),
            }
        };

        // process attributes
        t.process_closure_attrs(&inputs, output, meta)?;
        Ok(t)
    }

    /// Translate the body of a function.
    pub fn new(
        env: &'def Environment<'tcx>,
        meta: &ProcedureMeta,
        proc: Procedure<'tcx>,
        attrs: &'a [&'a ast::ast::AttrItem],
        ty_translator: &'def TypeTranslator<'def, 'tcx>,
        proc_registry: &'a ProcedureScope<'def>,
        const_registry: &'a ConstScope<'def>,
    ) -> Result<Self, TranslationError> {
        let mut translated_fn = radium::FunctionBuilder::new(&meta.name, &meta.spec_name);

        // TODO can we avoid the leak
        let proc: &'def Procedure = &*Box::leak(Box::new(proc));

        let body = proc.get_mir();
        Self::dump_body(body);

        let ty: ty::EarlyBinder<Ty<'tcx>> = env.tcx().type_of(proc.get_id());
        let ty = ty.instantiate_identity();
        // substs are the generic args of this function (including lifetimes)
        // sig is the function signature
        let sig = match ty.kind() {
            TyKind::FnDef(_def, _args) => {
                assert!(ty.is_fn());
                ty.fn_sig(env.tcx())
            },
            _ => panic!("can not handle non-fns"),
        };

        let sig = normalize_in_function(proc.get_id(), env.tcx(), sig)?;
        info!("Function signature: {:?}", sig);

        let info = match PoloniusInfo::new(env, proc) {
            Ok(info) => info,
            Err(err) => return Err(TranslationError::UnknownError(format!("{:?}", err))),
        };

        // TODO: avoid leak
        let info: &'def PoloniusInfo = &*Box::leak(Box::new(info));

        let params = proc.get_type_params();
        info!("Function generic args: {:?}", params);

        // dump graphviz files
        // color code: red: dying loan, pink: becoming a zombie; green: is zombie
        if rrconfig::dump_borrowck_info() {
            dump_borrowck_info(env, proc.get_id(), info);
        }

        let (inputs, output, universal_lifetimes) = Self::process_lifetimes_of_args(env, params, sig, body);
        info!("Have lifetime parameters: {:?}", universal_lifetimes);
        info!("inputs: {:?}, output: {:?}", inputs, output);

        // add universal lifetimes to the spec
        for (lft, name) in universal_lifetimes.values() {
            translated_fn
                .add_universal_lifetime(name.clone(), lft.to_string())
                .map_err(TranslationError::UnknownError)?;
        }

        let mut inclusion_tracker = InclusionTracker::new(info);
        // add placeholder subsets
        let initial_point: facts::Point = facts::Point {
            location: BasicBlock::from_u32(0).start_location(),
            typ: facts::PointType::Start,
        };
        for (r1, r2) in &info.borrowck_in_facts.known_placeholder_subset {
            inclusion_tracker.add_static_inclusion(*r1, *r2, info.interner.get_point_index(&initial_point));
        }

        // enter the procedure
        let universal_lifetime_map: HashMap<_, _> =
            universal_lifetimes.into_iter().map(|(x, y)| (x, y.0)).collect();

        let type_scope = TypeTranslationScope::new(proc.get_id(), params, universal_lifetime_map)?;
        // add generic args to the fn
        for t in type_scope.generic_scope.iter().flatten() {
            translated_fn.add_generic_type(t.clone());
        }

        let mut t = Self {
            env,
            proc,
            info,
            translated_fn,
            inclusion_tracker,
            procedure_registry: proc_registry,
            attrs,
            ty_translator: LocalTypeTranslator::new(ty_translator, type_scope),
            const_registry,
            inputs: inputs.clone(),
        };

        // add universal constraints
        let universal_constraints = t.get_relevant_universal_constraints();
        info!("univeral constraints: {:?}", universal_constraints);
        for (lft1, lft2) in universal_constraints {
            t.translated_fn
                .add_universal_lifetime_constraint(lft1, lft2)
                .map_err(TranslationError::UnknownError)?;
        }

        // process attributes
        t.process_attrs(inputs.as_slice(), output)?;

        Ok(t)
    }

    /// Filter the "interesting" constraints between universal lifetimes that need to hold
    /// (this does not include the constraints that need to hold for all universal lifetimes,
    /// e.g. that they outlive the function lifetime and are outlived by 'static).
    fn get_relevant_universal_constraints(&mut self) -> Vec<(radium::UniversalLft, radium::UniversalLft)> {
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let placeholder_subset = &input_facts.known_placeholder_subset;

        let root_location = Location {
            block: BasicBlock::from_u32(0),
            statement_index: 0,
        };
        let root_point = self.info.interner.get_point_index(&facts::Point {
            location: root_location,
            typ: facts::PointType::Start,
        });

        let mut universal_constraints = Vec::new();

        for (r1, r2) in placeholder_subset {
            if let polonius_info::RegionKind::Universal(uk1) = info.get_region_kind(*r1) {
                if let polonius_info::RegionKind::Universal(uk2) = info.get_region_kind(*r2) {
                    // if LHS is static, ignore.
                    if uk1 == polonius_info::UniversalRegionKind::Static {
                        continue;
                    }
                    // if RHS is the function lifetime, ignore
                    if uk2 == polonius_info::UniversalRegionKind::Function {
                        continue;
                    }

                    let to_universal = || {
                        let x = self.to_universal_lft(uk1, *r2)?;
                        let y = self.to_universal_lft(uk2, *r1)?;
                        Some((x, y))
                    };
                    // else, add this constraint
                    // for the purpose of this analysis, it is fine to treat it as a dynamic inclusion
                    if let Some((x, y)) = to_universal() {
                        self.inclusion_tracker.add_dynamic_inclusion(*r1, *r2, root_point);
                        universal_constraints.push((x, y));
                    };
                }
            }
        }
        universal_constraints
    }

    /// Parse and process attributes of this closure.
    fn process_closure_attrs<'b>(
        &mut self,
        normalized_inputs: &[Ty<'tcx>],
        normalized_output: Ty<'tcx>,
        meta: ClosureMetaInfo<'b, 'tcx, 'def>,
    ) -> Result<(), TranslationError> {
        trace!("entering process_closure_attrs");
        let v = self.attrs;

        info!("inputs: {:?}, output: {:?}", normalized_inputs, normalized_output);
        let mut translated_arg_types: Vec<radium::Type<'def>> = Vec::new();
        for arg in normalized_inputs {
            let translated: radium::Type<'def> = self.ty_translator.translate_type_no_normalize(*arg)?;
            translated_arg_types.push(translated);
        }
        let translated_ret_type: radium::Type<'def> =
            // output is already normalized
            self.ty_translator.translate_type_no_normalize(normalized_output)?;
        info!("translated function type: {:?} → {}", translated_arg_types, translated_ret_type);

        let parser = rrconfig::attribute_parser();

        if parser.as_str() != "verbose" {
            trace!("leaving process_closure_attrs");
            return Err(TranslationError::UnknownAttributeParser(parser));
        }

        let ty_translator = &self.ty_translator;
        let mut parser = VerboseFunctionSpecParser::new(&translated_arg_types, &translated_ret_type, |lit| {
            ty_translator.translator.intern_literal(lit)
        });

        parser
            .parse_closure_spec(v, &mut self.translated_fn, meta, |x| ty_translator.make_tuple_use(x))
            .map_err(TranslationError::AttributeError)?;

        trace!("leaving process_closure_attrs");
        Ok(())
    }

    /// Parse and process attributes of this function.
    fn process_attrs(
        &mut self,
        normalized_inputs: &[Ty<'tcx>],
        normalized_output: Ty<'tcx>,
    ) -> Result<(), TranslationError> {
        let v = self.attrs;

        info!("inputs: {:?}, output: {:?}", normalized_inputs, normalized_output);
        let mut translated_arg_types: Vec<radium::Type<'def>> = Vec::new();
        for arg in normalized_inputs {
            let translated: radium::Type<'def> = self.ty_translator.translate_type_no_normalize(*arg)?;
            translated_arg_types.push(translated);
        }
        let translated_ret_type: radium::Type<'def> =
            self.ty_translator.translate_type_no_normalize(normalized_output)?;
        info!("translated function type: {:?} → {}", translated_arg_types, translated_ret_type);

        let parser = rrconfig::attribute_parser();
        match parser.as_str() {
            "verbose" => {
                let ty_translator = &self.ty_translator;
                let mut parser: VerboseFunctionSpecParser<'_, 'def, _> =
                    VerboseFunctionSpecParser::new(&translated_arg_types, &translated_ret_type, |lit| {
                        ty_translator.translator.intern_literal(lit)
                    });

                parser
                    .parse_function_spec(v, &mut self.translated_fn)
                    .map_err(TranslationError::AttributeError)?;

                Ok(())
            },
            _ => Err(TranslationError::UnknownAttributeParser(parser)),
        }
    }

    // TODO refactor/ move
    fn to_universal_lft(
        &self,
        k: polonius_info::UniversalRegionKind,
        r: Region,
    ) -> Option<radium::UniversalLft> {
        match k {
            polonius_info::UniversalRegionKind::Function => Some(radium::UniversalLft::Function),
            polonius_info::UniversalRegionKind::Static => Some(radium::UniversalLft::Static),

            polonius_info::UniversalRegionKind::Local => self
                .ty_translator
                .scope
                .borrow()
                .lookup_universal_region(r)
                .map(radium::UniversalLft::Local),

            polonius_info::UniversalRegionKind::External => self
                .ty_translator
                .scope
                .borrow()
                .lookup_universal_region(r)
                .map(radium::UniversalLft::External),
        }
    }

    /// Translation that only generates a specification.
    pub fn generate_spec(self) -> radium::FunctionSpec<'def> {
        self.translated_fn.into()
    }

    /// Generate a string identifier for a Local.
    /// Tries to find the Rust source code name of the local, otherwise simply enumerates.
    /// `used_names` keeps track of the Rust source code names that have already been used.
    fn make_local_name(mir_body: &Body<'tcx>, local: Local, used_names: &mut HashSet<String>) -> String {
        if let Some(mir_name) = Self::find_name_for_local(mir_body, local, used_names) {
            let name = strip_coq_ident(&mir_name);
            used_names.insert(mir_name);
            name
        } else {
            let mut name = "__".to_owned();
            name.push_str(&local.index().to_string());
            name
        }
    }

    /// Find a source name for a local of a MIR body, if possible.
    fn find_name_for_local(
        body: &mir::Body<'tcx>,
        local: mir::Local,
        used_names: &HashSet<String>,
    ) -> Option<String> {
        let debug_info = &body.var_debug_info;

        for dbg in debug_info {
            let name = &dbg.name;
            let val = &dbg.value;
            match *val {
                VarDebugInfoContents::Place(l) => {
                    // make sure that the place projection is empty -- otherwise this might just
                    // refer to the capture of a closure
                    if let Some(this_local) = l.as_local() {
                        if this_local == local {
                            // around closures, multiple symbols may be mapped to the same name.
                            // To prevent this from happening, we check that the name hasn't been
                            // used already
                            // TODO: find better solution
                            if !used_names.contains(name.as_str()) {
                                return Some(name.as_str().to_owned());
                            }
                        }
                    }
                },
                VarDebugInfoContents::Const(_) => {
                    // is this case used when constant propagation happens during MIR construction?
                },
            }
        }

        None
    }

    fn dump_body(body: &Body) {
        // TODO: print to file
        let basic_blocks = &body.basic_blocks;
        for (bb_idx, bb) in basic_blocks.iter_enumerated() {
            Self::dump_basic_block(bb_idx, bb);
        }
    }

    /// Dump a basic block as info debug output.
    fn dump_basic_block(bb_idx: BasicBlock, bb: &BasicBlockData) {
        info!("Basic block {:?}:", bb_idx);
        let mut i = 0;
        for s in &bb.statements {
            info!("{}\t{:?}", i, s);
            i += 1;
        }
        info!("{}\t{:?}", i, bb.terminator());
    }

    pub fn translate(mut self) -> Result<radium::Function<'def>, TranslationError> {
        let body = self.proc.get_mir();

        // analyze which locals are used for the result of checked-ops, because we will
        // override their types (eliminating the tuples)
        let mut checked_op_analyzer = CheckedOpLocalAnalysis::new(self.env.tcx(), body);
        checked_op_analyzer.analyze();
        let checked_op_locals = checked_op_analyzer.results();

        // map to translate between locals and the string names we use in radium::
        let mut radium_name_map: HashMap<Local, String> = HashMap::new();

        let local_decls = &body.local_decls;
        info!("Have {} local decls\n", local_decls.len());

        let debug_info = &body.var_debug_info;
        info!("using debug info: {:?}", debug_info);

        let mut return_synty = radium::SynType::Unit; // default
        let mut fn_locals = Vec::new();
        let mut opt_return_name =
            Err(TranslationError::UnknownError("could not find local for return value".to_owned()));
        let mut used_names = HashSet::new();
        let mut arg_tys = Vec::new();

        // go over local_decls and create the right radium:: stack layout
        for (local, local_decl) in local_decls.iter_enumerated() {
            let kind = body.local_kind(local);

            let ty: Ty<'tcx> = if let Some(rewritten_ty) = checked_op_locals.get(&local) {
                *rewritten_ty
            } else {
                local_decl.ty
            };

            // check if the type is of a spec fn -- in this case, we can skip this temporary
            if let TyKind::Closure(id, _) = ty.kind() {
                if self.procedure_registry.lookup_function_mode(*id).map_or(false, ProcedureMode::is_ignore) {
                    // this is a spec fn
                    info!("skipping local which has specfn closure type: {:?}", local);
                    continue;
                }
            }

            // type:
            let tr_ty = self.ty_translator.translate_type(ty)?;
            let st = tr_ty.get_syn_type();

            let name = Self::make_local_name(body, local, &mut used_names);
            radium_name_map.insert(local, name.clone());

            fn_locals.push((local, name.clone(), tr_ty));

            match kind {
                LocalKind::Arg => {
                    self.translated_fn.code.add_argument(&name, st);
                    arg_tys.push(ty);
                },
                //LocalKind::Var => translated_fn.code.add_local(&name, st),
                LocalKind::Temp => self.translated_fn.code.add_local(&name, st),
                LocalKind::ReturnPointer => {
                    return_synty = st.clone();
                    self.translated_fn.code.add_local(&name, st);
                    opt_return_name = Ok(name);
                },
            }
        }
        info!("radium name map: {:?}", radium_name_map);
        // create the function
        let return_name = opt_return_name?;

        // add lifetime parameters to the map
        let inputs2 = self.inputs.clone();
        let initial_constraints =
            self.get_initial_universal_arg_constraints(inputs2.as_slice(), arg_tys.as_slice());
        info!("initial constraints: {:?}", initial_constraints);

        let translator = BodyTranslator {
            env: self.env,
            proc: self.proc,
            info: self.info,
            variable_map: radium_name_map,
            translated_fn: self.translated_fn,
            return_name,
            return_synty,
            inclusion_tracker: self.inclusion_tracker,
            collected_procedures: HashMap::new(),
            procedure_registry: self.procedure_registry,
            attrs: self.attrs,
            local_lifetimes: Vec::new(),
            bb_queue: Vec::new(),
            processed_bbs: HashSet::new(),
            ty_translator: self.ty_translator,
            loop_specs: HashMap::new(),
            fn_locals,
            checked_op_temporaries: checked_op_locals,
            const_registry: self.const_registry,
            collected_statics: HashSet::new(),
        };
        translator.translate(&initial_constraints)
    }

    /// Determine initial constraints between universal regions and local place regions.
    /// Returns an initial mapping for the name _map that initializes place regions of arguments
    /// with universals.
    fn get_initial_universal_arg_constraints(
        &mut self,
        _sig_args: &[Ty<'tcx>],
        _local_args: &[Ty<'tcx>],
    ) -> Vec<(polonius_info::AtomicRegion, polonius_info::AtomicRegion)> {
        // Polonius generates a base subset constraint uregion ⊑ pregion.
        // We turn that into pregion = uregion, as we do strong updates at the top-level.
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let subset_base = &input_facts.subset_base;

        let root_location = Location {
            block: BasicBlock::from_u32(0),
            statement_index: 0,
        };
        let root_point = self.info.interner.get_point_index(&facts::Point {
            location: root_location,
            typ: facts::PointType::Start,
        });

        // TODO: for nested references, this doesn't really seem to work.
        // Problem is that we don't have constraints for the mapping of nested references.
        // Potentially, we should instead just equalize the types

        let mut initial_arg_mapping = Vec::new();
        for (r1, r2, _) in subset_base {
            let lft1 = self.info.mk_atomic_region(*r1);
            let lft2 = self.info.mk_atomic_region(*r2);

            let polonius_info::AtomicRegion::Universal(polonius_info::UniversalRegionKind::Local, _) = lft1
            else {
                continue;
            };

            // this is a constraint we care about here, add it
            if self.inclusion_tracker.check_inclusion(*r1, *r2, root_point) {
                continue;
            }

            self.inclusion_tracker.add_static_inclusion(*r1, *r2, root_point);
            self.inclusion_tracker.add_static_inclusion(*r2, *r1, root_point);

            assert!(matches!(lft2, polonius_info::AtomicRegion::PlaceRegion(_)));

            initial_arg_mapping.push((lft1, lft2));
        }
        initial_arg_mapping
    }

    #[allow(clippy::unused_self)]
    fn get_initial_universal_arg_constraints2(
        &mut self,
        sig_args: &[Ty<'tcx>],
        local_args: &[Ty<'tcx>],
    ) -> Vec<(polonius_info::AtomicRegion, polonius_info::AtomicRegion)> {
        // Polonius generates a base subset constraint uregion ⊑ pregion.
        // We turn that into pregion = uregion, as we do strong updates at the top-level.
        assert!(sig_args.len() == local_args.len());

        // TODO: implement a bitypefolder to solve this issue.
        Vec::new()
    }
}

/**
 * Struct that keeps track of all information necessary to translate a MIR Body to a `radium::Function`.
 * `'a` is the lifetime of the translator and ends after translation has finished.
 * `'def` is the lifetime of the generated code (the code may refer to struct defs).
 * `'tcx` is the lifetime of the rustc tctx.
 */
struct BodyTranslator<'a, 'def, 'tcx> {
    env: &'def Environment<'tcx>,
    /// this needs to be annotated with the right borrowck things
    proc: &'def Procedure<'tcx>,
    /// maps locals to variable names
    variable_map: HashMap<Local, String>,
    /// the Caesium function buildder
    translated_fn: radium::FunctionBuilder<'def>,
    /// name of the return variable
    return_name: String,
    /// syntactic type of the thing to return
    return_synty: radium::SynType,
    /// all the other procedures used by this function, and:
    /// (code_loc_parameter_name, spec_name, type_inst, syntype_of_all_args)
    collected_procedures:
        HashMap<(DefId, FnGenericKey<'tcx>), (String, String, Vec<radium::Type<'def>>, Vec<radium::SynType>)>,
    /// used statics
    collected_statics: HashSet<DefId>,

    /// tracking lifetime inclusions for the generation of lifetime inclusions
    inclusion_tracker: InclusionTracker<'a, 'tcx>,

    /// registry of other procedures
    procedure_registry: &'a ProcedureScope<'def>,
    /// scope of used consts
    const_registry: &'a ConstScope<'def>,
    /// attributes on this function
    attrs: &'a [&'a ast::ast::AttrItem],
    /// polonius info for this function
    info: &'a PoloniusInfo<'a, 'tcx>,
    /// local lifetimes: the LHS is the lifetime name, the RHS are the super lifetimes
    local_lifetimes: Vec<(radium::specs::Lft, Vec<radium::specs::Lft>)>,
    /// data structures for tracking which basic blocks still need to be translated
    /// (we only translate the basic blocks which are actually reachable, in particular when
    /// skipping unwinding)
    bb_queue: Vec<BasicBlock>,
    /// set of already processed blocks
    processed_bbs: HashSet<BasicBlock>,
    /// translator for types
    ty_translator: LocalTypeTranslator<'a, 'def, 'tcx>,

    /// map of loop heads to their optional spec closure defid
    loop_specs: HashMap<BasicBlock, Option<DefId>>,

    /// relevant locals: (local, name, type)
    fn_locals: Vec<(Local, String, radium::Type<'def>)>,

    /// result temporaries of checked ops that we rewrite
    /// we assume that this place is typed at (result_type, bool)
    /// and rewrite accesses to the first component to directly use the place,
    /// while rewriting accesses to the second component to true.
    /// TODO: once we handle panics properly, we should use a different translation.
    /// NOTE: we only rewrite for uses, as these are the only places these are used.
    checked_op_temporaries: HashMap<Local, Ty<'tcx>>,
}

impl<'a, 'def: 'a, 'tcx: 'def> BodyTranslator<'a, 'def, 'tcx> {
    /// Main translation function that actually does the translation and returns a `radium::Function`
    /// if successful.
    pub fn translate(
        mut self,
        initial_constraints: &Vec<(polonius_info::AtomicRegion, polonius_info::AtomicRegion)>,
    ) -> Result<radium::Function<'def>, TranslationError> {
        // add loop info
        let loop_info = self.proc.loop_info();
        loop_info.dump_body_head();

        // translate the function's basic blocks
        let basic_blocks = &self.proc.get_mir().basic_blocks;

        // first translate the initial basic block; we add some additional annotations to the front
        let initial_bb_idx = BasicBlock::from_u32(0);
        if let Some(bb) = basic_blocks.get(initial_bb_idx) {
            let mut translated_bb = self.translate_basic_block(initial_bb_idx, bb)?;
            // push annotation for initial constraints that relate argument's place regions to universals
            for (r1, r2) in initial_constraints {
                translated_bb = radium::Stmt::Annot {
                    a: radium::Annotation::CopyLftName(
                        self.format_atomic_region(r1),
                        self.format_atomic_region(r2),
                    ),
                    s: Box::new(translated_bb),
                    why: Some("initialization".to_owned()),
                };
            }
            self.translated_fn.code.add_basic_block(initial_bb_idx.as_usize(), translated_bb);
        } else {
            info!("No basic blocks");
        }

        while let Some(bb_idx) = self.bb_queue.pop() {
            let bb = &basic_blocks[bb_idx];
            let translated_bb = self.translate_basic_block(bb_idx, bb)?;
            self.translated_fn.code.add_basic_block(bb_idx.as_usize(), translated_bb);
        }

        // assume that all generics are layoutable
        {
            let scope = self.ty_translator.scope.borrow();
            for ty in scope.generic_scope.iter().flatten() {
                self.translated_fn.assume_synty_layoutable(radium::SynType::Literal(ty.syn_type.clone()));
            }
        }
        // assume that all used literals are layoutable
        for g in self.ty_translator.scope.borrow().shim_uses.values() {
            self.translated_fn.assume_synty_layoutable(g.generate_syn_type_term());
        }
        // assume that all used tuples are layoutable
        for g in &self.ty_translator.scope.borrow().tuple_uses {
            self.translated_fn.assume_synty_layoutable(g.generate_syn_type_term());
        }

        // TODO: process self.loop_specs
        // - collect the relevant bb -> def_id mappings for this function (so we can eventually generate the
        //   definition)
        // - have a function that takes the def_id and then parses the attributes into a loop invariant
        for (head, did) in &self.loop_specs {
            let spec = self.parse_attributes_on_loop_spec_closure(*head, *did);
            self.translated_fn.register_loop_invariant(head.as_usize(), spec);
        }

        // generate dependencies on other procedures.
        for (loc_name, spec_name, params, sts) in self.collected_procedures.values() {
            self.translated_fn.require_function(
                loc_name.clone(),
                spec_name.clone(),
                params.clone(),
                sts.clone(),
            );
        }

        // generate dependencies on statics
        for did in &self.collected_statics {
            let s = &self.const_registry.statics[did];
            self.translated_fn.require_static(s.clone());
        }

        Ok(self.translated_fn.into())
    }

    /// Determine initial constraints between universal regions and local place regions.
    /// Returns an initial mapping for the name _map that initializes place regions of arguments
    /// with universals.
    fn get_initial_universal_arg_constraints(
        &mut self,
        _sig_args: &[Ty<'tcx>],
        _local_args: &[Ty<'tcx>],
    ) -> Vec<(polonius_info::AtomicRegion, polonius_info::AtomicRegion)> {
        // Polonius generates a base subset constraint uregion ⊑ pregion.
        // We turn that into pregion = uregion, as we do strong updates at the top-level.
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let subset_base = &input_facts.subset_base;

        let root_location = Location {
            block: BasicBlock::from_u32(0),
            statement_index: 0,
        };
        let root_point = self.info.interner.get_point_index(&facts::Point {
            location: root_location,
            typ: facts::PointType::Start,
        });

        // TODO: for nested references, this doesn't really seem to work.
        // Problem is that we don't have constraints for the mapping of nested references.
        // Potentially, we should instead just equalize the types

        let mut initial_arg_mapping = Vec::new();
        for (r1, r2, _) in subset_base {
            let lft1 = self.info.mk_atomic_region(*r1);
            let lft2 = self.info.mk_atomic_region(*r2);

            let polonius_info::AtomicRegion::Universal(polonius_info::UniversalRegionKind::Local, _) = lft1
            else {
                continue;
            };

            // this is a constraint we care about here, add it
            if self.inclusion_tracker.check_inclusion(*r1, *r2, root_point) {
                continue;
            }

            self.inclusion_tracker.add_static_inclusion(*r1, *r2, root_point);
            self.inclusion_tracker.add_static_inclusion(*r2, *r1, root_point);

            assert!(matches!(lft2, polonius_info::AtomicRegion::PlaceRegion(_)));

            initial_arg_mapping.push((lft1, lft2));
        }
        initial_arg_mapping
    }

    /// Generate a key for generics to index into our map of other required procedures.
    fn generate_procedure_inst_key(
        &self,
        ty_params: ty::GenericArgsRef<'tcx>,
    ) -> Result<FnGenericKey<'tcx>, TranslationError> {
        // erase parameters to their syntactic types
        let mut key = Vec::new();
        let mut region_eraser = TyRegionEraseFolder::new(self.env.tcx());
        for p in ty_params {
            match p.unpack() {
                ty::GenericArgKind::Lifetime(_) => {
                    // lifetimes are not relevant here
                },
                ty::GenericArgKind::Type(t) => {
                    // TODO: this should erase to the syntactic type.
                    // Is erasing regions enough for that?
                    let t_erased = t.fold_with(&mut region_eraser);
                    key.push(t_erased);
                },
                ty::GenericArgKind::Const(_c) => {
                    return Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does not support const generics".to_owned(),
                    });
                },
            }
        }
        Ok(key)
    }

    /// Internally register that we have used a procedure with a particular instantiation of generics, and
    /// return the code parameter name.
    fn register_use_procedure(
        &mut self,
        did: DefId,
        ty_params: ty::GenericArgsRef<'tcx>,
    ) -> Result<String, TranslationError> {
        trace!("enter register_use_procedure did={:?} ty_params={:?}", did, ty_params);

        let key = self.generate_procedure_inst_key(ty_params)?;
        let tup = (did, key);

        if let Some((n, ..)) = self.collected_procedures.get(&tup) {
            trace!("leave register_use_procedure");

            return Ok(n.to_owned());
        }

        // lookup the name in the procedure registry
        let name = self
            .procedure_registry
            .lookup_function_mangled_name(did)
            .ok_or_else(|| TranslationError::UnknownProcedure(format!("{:?}", did)))?;

        let spec_name = self
            .procedure_registry
            .lookup_function_spec_name(did)
            .ok_or_else(|| TranslationError::UnknownProcedure(format!("{:?}", did)))?;

        let mut mangled_name = name.to_owned();
        let mut translated_params = Vec::new();

        // TODO: maybe come up with some better way to generate names
        info!("register_use_procedure: translating args {:?}", tup.1);

        for p in &tup.1 {
            mangled_name.push_str(format!("_{}", p).as_str());

            let translated_ty = self.ty_translator.translate_type(*p)?;

            translated_params.push(translated_ty);
        }

        let mangled_name = strip_coq_ident(&mangled_name);

        // also gather all the layouts of the arguments.
        let full_ty: ty::EarlyBinder<Ty<'tcx>> = self.env.tcx().type_of(did);
        let full_ty: Ty<'tcx> = full_ty.instantiate_identity();

        let mut normalized_inputs = Vec::new();
        let mut syntypes = Vec::new();

        match full_ty.kind() {
            ty::TyKind::FnDef(_, _) => {
                let sig = full_ty.fn_sig(self.env.tcx());
                let sig = normalize_in_function(did, self.env.tcx(), sig)?;

                for ty in sig.inputs().skip_binder() {
                    normalized_inputs.push(*ty);
                }
            },

            ty::TyKind::Closure(_, args) => {
                let clos_args = args.as_closure();
                let sig = clos_args.sig();
                let sig = normalize_in_function(did, self.env.tcx(), sig)?;
                let pre_sig = sig.skip_binder();
                // we also need to add the closure argument here
                // in this case, we need to patch the region first

                let tuple_ty = clos_args.tupled_upvars_ty();

                match clos_args.kind() {
                    ty::ClosureKind::Fn | ty::ClosureKind::FnMut => {
                        syntypes.push(radium::SynType::Ptr);
                    },
                    ty::ClosureKind::FnOnce => {
                        normalized_inputs.push(tuple_ty);
                    },
                }

                for ty in pre_sig.inputs() {
                    normalized_inputs.push(*ty);
                }
            },

            _ => unimplemented!(),
        }

        //info!("substs: {:?}, inputs {:?} ", ty_params, inputs);
        for i in &normalized_inputs {
            // need to wrap it, because there's no Subst instance for Ty
            let i = ty::EarlyBinder::bind(*i);
            let ty = i.instantiate(self.env.tcx(), ty_params);
            let t = self.ty_translator.translate_type_to_syn_type_no_normalize(ty)?;
            syntypes.push(t);
        }

        let loc_name = format!("{}_loc", mangled_name);

        info!(
            "Registered procedure instance {} of {:?} with {:?} and layouts {:?}",
            mangled_name, did, translated_params, syntypes
        );

        self.collected_procedures
            .insert(tup, (loc_name.clone(), spec_name.to_owned(), translated_params, syntypes));

        trace!("leave register_use_procedure");
        Ok(loc_name)
    }

    /// Enqueues a basic block for processing, if it has not already been processed,
    /// and marks it as having been processed.
    fn enqueue_basic_block(&mut self, bb: BasicBlock) {
        if !self.processed_bbs.contains(&bb) {
            self.bb_queue.push(bb);
            self.processed_bbs.insert(bb);
        }
    }

    /// Format an atomic region, using the naming info for universal lifetimes available in the current
    /// context.
    fn format_atomic_region(&self, r: &polonius_info::AtomicRegion) -> String {
        self.ty_translator.format_atomic_region(r)
    }

    fn format_region(&self, r: facts::Region) -> String {
        let lft = self.info.mk_atomic_region(r);
        self.format_atomic_region(&lft)
    }

    /// Parse the attributes on spec closure `did` as loop annotations and add it as an invariant
    /// to the generated code.
    fn parse_attributes_on_loop_spec_closure(
        &self,
        loop_head: BasicBlock,
        did: Option<DefId>,
    ) -> radium::LoopSpec {
        // for now: just make invariants True.

        // need to do:
        // - find out the locals in the right order, make parameter names for them. based on their type and
        //   initialization status, get the refinement type.
        // - output/pretty-print this map when generating the typing proof of each function. [done]
        //  + should not be a separate definition, but rather a "set (.. := ...)" with a marker type so
        //    automation can find it.

        // representation of loop invariants:
        // - introduce parameters for them.

        let mut rfn_binders = Vec::new();
        let prop_body = radium::IProp::True;

        // determine invariant on initialization:
        // - we need this both for the refinement invariant (though this could be removed if we make uninit
        //   generic over the refinement)
        // - in order to establish the initialization invariant in each loop iteration, since we don't have
        //   proper subtyping for uninit => maybe we could fix this too by making uninit variant in the
        //   refinement type? then we could have proper subtyping lemmas.
        //  + to bring it to the right refinement type initially, maybe have some automation /
        //  annotation
        // TODO: consider changing it like that.
        //
        // Note that StorageDead will not help us for determining initialization/ making it invariant, since
        // it only applies to full stack slots, not individual paths. one thing that makes it more
        // complicated in the frontend: initialization may in practice also be path-dependent.
        //  - this does not cause issues with posing a too strong loop invariant,
        //  - but this poses an issue for annotations
        //
        //

        // get locals
        for (_, name, ty) in &self.fn_locals {
            // get the refinement type
            let mut rfn_ty = ty.get_rfn_type(&[]);
            // wrap it in place_rfn, since we reason about places
            rfn_ty = coq::Type::PlaceRfn(Box::new(rfn_ty));

            // determine their initialization status
            //let initialized = true; // TODO
            // determine the actual refinement type for the current initialization status.

            let rfn_name = coq::Name::Named(format!("r_{}", name));
            rfn_binders.push(radium::specs::CoqBinder::new(rfn_name, rfn_ty));
        }

        // TODO what do we do about stuff connecting borrows?
        if let Some(did) = did {
            let attrs = self.env.get_attributes(did);
            info!("attrs for loop {:?}: {:?}", loop_head, attrs);
        } else {
            info!("no attrs for loop {:?}", loop_head);
        }

        let pred = radium::IPropPredicate::new(rfn_binders, prop_body);
        radium::LoopSpec {
            func_predicate: pred,
        }
    }

    /// Checks whether a place access descends below a reference.
    fn check_place_below_reference(&self, place: &Place<'tcx>) -> bool {
        if self.checked_op_temporaries.contains_key(&place.local) {
            // temporaries are never below references
            return false;
        }

        for (pl, _) in place.iter_projections() {
            // check if the current ty is a reference that we then descend under with proj
            let cur_ty_kind = pl.ty(&self.proc.get_mir().local_decls, self.env.tcx()).ty.kind();
            if let TyKind::Ref(_, _, _) = cur_ty_kind {
                return true;
            }
        }

        false
    }

    fn get_assignment_strong_update_constraints(
        &mut self,
        loc: Location,
    ) -> HashSet<(Region, Region, PointIndex)> {
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let subset_base = &input_facts.subset_base;

        let mut constraints = HashSet::new();
        // Polonius subset constraint are spawned for the midpoint
        let midpoint = self.info.interner.get_point_index(&facts::Point {
            location: loc,
            typ: facts::PointType::Mid,
        });

        // for strong update: emit mutual equalities
        // TODO: alternative implementation: structurally compare regions in LHS type and RHS type
        for (s1, s2, point) in subset_base {
            if *point == midpoint {
                let lft1 = self.info.mk_atomic_region(*s1);
                let lft2 = self.info.mk_atomic_region(*s2);

                // We only care about inclusions into a place lifetime.
                // Moreover, we want to filter out the universal inclusions which are always
                // replicated at every point.
                if lft2.is_place() && !lft1.is_universal() {
                    // take this constraint and the reverse constraint
                    constraints.insert((*s1, *s2, *point));
                    //constraints.insert((*s2, *s1, *point));
                }
            }
        }
        constraints
    }

    fn get_assignment_weak_update_constraints(
        &mut self,
        loc: Location,
    ) -> HashSet<(Region, Region, PointIndex)> {
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let subset_base = &input_facts.subset_base;

        let mut constraints = HashSet::new();
        // Polonius subset constraint are spawned for the midpoint
        let midpoint = self.info.interner.get_point_index(&facts::Point {
            location: loc,
            typ: facts::PointType::Mid,
        });

        // for weak updates: should mirror the constraints generated by Polonius
        for (s1, s2, point) in subset_base {
            if *point == midpoint {
                // take this constraint
                // TODO should there be exceptions to this?

                if !self.inclusion_tracker.check_inclusion(*s1, *s2, *point) {
                    // only add it if it does not hold already, since we will enforce this
                    // constraint dynamically.
                    constraints.insert((*s1, *s2, *point));
                }
            }
        }
        constraints
    }

    /// Split the type of a function operand of a call expression to a base type and an instantiation for
    /// generics.
    fn call_expr_op_split_inst(
        &self,
        constant: &Constant<'tcx>,
    ) -> Result<(DefId, ty::PolyFnSig<'tcx>, ty::GenericArgsRef<'tcx>, ty::PolyFnSig<'tcx>), TranslationError>
    {
        match constant.literal {
            ConstantKind::Ty(c) => {
                match c.ty().kind() {
                    TyKind::FnDef(def, args) => {
                        let ty: ty::EarlyBinder<Ty<'tcx>> = self.env.tcx().type_of(def);

                        let ty_ident = ty.instantiate_identity();
                        assert!(ty_ident.is_fn());
                        let ident_sig = ty_ident.fn_sig(self.env.tcx());

                        let ty_instantiated = ty.instantiate(self.env.tcx(), args.as_slice());
                        let instantiated_sig = ty_instantiated.fn_sig(self.env.tcx());

                        Ok((*def, ident_sig, args, instantiated_sig))
                    },
                    // TODO handle FnPtr, closure
                    _ => Err(TranslationError::Unimplemented {
                        description: "implement function pointers".to_owned(),
                    }),
                }
            },
            ConstantKind::Val(_, ty) => {
                match ty.kind() {
                    TyKind::FnDef(def, args) => {
                        let ty: ty::EarlyBinder<Ty<'tcx>> = self.env.tcx().type_of(def);

                        let ty_ident = ty.instantiate_identity();
                        assert!(ty_ident.is_fn());
                        let ident_sig = ty_ident.fn_sig(self.env.tcx());

                        let ty_instantiated = ty.instantiate(self.env.tcx(), args.as_slice());
                        let instantiated_sig = ty_instantiated.fn_sig(self.env.tcx());

                        Ok((*def, ident_sig, args, instantiated_sig))
                    },
                    // TODO handle FnPtr, closure
                    _ => Err(TranslationError::Unimplemented {
                        description: "implement function pointers".to_owned(),
                    }),
                }
            },
            ConstantKind::Unevaluated(_, _) => Err(TranslationError::Unimplemented {
                description: "implement ConstantKind::Unevaluated".to_owned(),
            }),
        }
    }

    /// Find the optional `DefId` of the closure giving the invariant for the loop with head `head_bb`.
    fn find_loop_spec_closure(&self, head_bb: BasicBlock) -> Result<Option<DefId>, TranslationError> {
        let bodies = self.proc.loop_info().get_loop_body(head_bb);
        let basic_blocks = &self.proc.get_mir().basic_blocks;

        // we go in order through the bodies in order to not stumble upon an annotation for a
        // nested loop!
        for body in bodies {
            // check that we did not go below a nested loop
            if self.proc.loop_info().get_loop_head(*body) == Some(head_bb) {
                // check the statements for an assignment
                let data = basic_blocks.get(*body).unwrap();
                for stmt in &data.statements {
                    if let StatementKind::Assign(box (pl, _)) = stmt.kind {
                        if let Some(did) = self.is_spec_closure_local(pl.local)? {
                            return Ok(Some(did));
                        }
                    }
                }
            }
        }

        Ok(None)
    }

    /// Translate a goto-like jump to `target`.
    fn translate_goto_like(
        &mut self,
        _loc: &Location,
        target: BasicBlock,
    ) -> Result<radium::Stmt, TranslationError> {
        self.enqueue_basic_block(target);
        let res_stmt = radium::Stmt::GotoBlock(target.as_usize());

        let loop_info = self.proc.loop_info();
        if loop_info.is_loop_head(target) && !self.loop_specs.contains_key(&target) {
            let spec_defid = self.find_loop_spec_closure(target)?;
            self.loop_specs.insert(target, spec_defid);
        }

        Ok(res_stmt)
    }

    /// Check if a call goes to `std::rt::begin_panic`
    fn is_call_destination_panic(&mut self, func: &Operand) -> bool {
        let Operand::Constant(box c) = func else {
            return false;
        };

        let ConstantKind::Val(_, ty) = c.literal else {
            return false;
        };

        let TyKind::FnDef(did, _) = ty.kind() else {
            return false;
        };

        if let Some(panic_id_std) =
            utils::try_resolve_did(self.env.tcx(), &["std", "panicking", "begin_panic"])
        {
            if panic_id_std == *did {
                return true;
            }
        } else {
            warn!("Failed to determine DefId of std::panicking::begin_panic");
        }

        if let Some(panic_id_core) = utils::try_resolve_did(self.env.tcx(), &["core", "panicking", "panic"]) {
            if panic_id_core == *did {
                return true;
            }
        } else {
            warn!("Failed to determine DefId of core::panicking::panic");
        }

        false
    }

    /// Registers a drop shim for a particular type for the translation.
    #[allow(clippy::unused_self)]
    const fn register_drop_shim_for(&self, _ty: Ty<'tcx>) {
        // TODO!
        //let drop_in_place_did: DefId = utils::try_resolve_did(self.env.tcx(), &["std", "ptr",
        // "drop_in_place"]).unwrap();

        //let x: ty::InstanceDef = ty::InstanceDef::DropGlue(drop_in_place_did, Some(ty));
        //let body: &'tcx mir::Body = self.env.tcx().mir_shims(x);

        //info!("Generated drop shim for {:?}", ty);
        //Self::dump_body(body);
    }

    fn compute_call_regions(
        &self,
        func: &Constant<'tcx>,
        loc: Location,
    ) -> Result<CallRegions, TranslationError> {
        let midpoint = self.info.interner.get_point_index(&facts::Point {
            location: loc,
            typ: facts::PointType::Mid,
        });

        // first identify substitutions for the early-bound regions
        let (target_did, sig, substs, _) = self.call_expr_op_split_inst(func)?;
        info!("calling function {:?}", target_did);
        let mut early_regions = Vec::new();
        info!("call substs: {:?} = {:?}, {:?}", func, sig, substs);
        for a in substs {
            if let ty::GenericArgKind::Lifetime(r) = a.unpack() {
                if let ty::RegionKind::ReVar(r) = r.kind() {
                    early_regions.push(r);
                }
            }
        }
        info!("call region instantiations (early): {:?}", early_regions);

        // this is a hack to identify the inference variables introduced for the
        // call's late-bound universals.
        // TODO: Can we get this information in a less hacky way?
        // One approach: compute the early + late bound regions for a given DefId, similarly to how
        // we do it when starting to translate a function
        // Problem: this doesn't give a straightforward way to compute their instantiation

        // now find all the regions that appear in type parameters we instantiate.
        // These are regions that the callee doesn't know about.
        let mut generic_regions = HashSet::new();
        let mut clos = |r: ty::Region<'tcx>, _| match r.kind() {
            ty::RegionKind::ReVar(rv) => {
                generic_regions.insert(rv);
                r
            },
            _ => r,
        };

        for a in substs {
            if let ty::GenericArgKind::Type(c) = a.unpack() {
                let mut folder = ty::fold::RegionFolder::new(self.env.tcx(), &mut clos);
                folder.fold_ty(c);
            }
        }
        info!("Regions of generic args: {:?}", generic_regions);

        // go over all region constraints initiated at this location
        let new_constraints = self.info.get_new_subset_constraints_at_point(midpoint);
        let mut new_regions = HashSet::new();
        let mut relevant_constraints = Vec::new();
        for (r1, r2) in &new_constraints {
            if matches!(self.info.get_region_kind(*r1), polonius_info::RegionKind::Unknown) {
                // this is probably a inference variable for the call
                new_regions.insert(*r1);
                relevant_constraints.push((*r1, *r2));
            }
            if matches!(self.info.get_region_kind(*r2), polonius_info::RegionKind::Unknown) {
                new_regions.insert(*r2);
                relevant_constraints.push((*r1, *r2));
            }
        }
        // first sort this to enable cycle resolution
        let mut new_regions_sorted: Vec<Region> = new_regions.iter().copied().collect();
        new_regions_sorted.sort();

        // identify the late-bound regions
        let mut late_regions = Vec::new();
        for r in &new_regions_sorted {
            // only take the ones which are not early bound and
            // which are not due to a generic (the callee doesn't care about generic regions)
            if !early_regions.contains(r) && !generic_regions.contains(r) {
                late_regions.push(*r);
            }
        }
        info!("call region instantiations (late): {:?}", late_regions);

        // Notes:
        // - if two of the call regions need to be equal due to constraints on the function, we define the one
        //   with the larger id in terms of the other one
        // - we ignore unidirectional subset constraints between call regions (these do not help in finding a
        //   solution if we take the transitive closure beforehand)
        // - if a call region needs to be equal to a local region, we directly define it in terms of the local
        //   region
        // - otherwise, it will be an intersection of local regions
        let mut new_regions_classification = HashMap::new();
        // compute transitive closure of constraints
        let relevant_constraints = polonius_info::compute_transitive_closure(relevant_constraints);
        for r in &new_regions_sorted {
            for (r1, r2) in &relevant_constraints {
                if *r2 != *r {
                    continue;
                }

                // i.e. (flipping it around when we are talking about lifetimes),
                // r needs to be a sublft of r1
                if relevant_constraints.contains(&(*r2, *r1)) {
                    // if r1 is also a new region and r2 is ordered before it, we will
                    // just define r1 in terms of r2
                    if new_regions.contains(r1) && r2.as_u32() < r1.as_u32() {
                        continue;
                    }
                    // need an equality constraint
                    new_regions_classification.insert(*r, CallRegionKind::EqR(*r1));
                    // do not consider the rest of the constraints as r is already
                    // fully specified
                    break;
                }

                // the intersection also needs to contain r1
                if new_regions.contains(r1) {
                    // we do not need this constraint, since we already computed the
                    // transitive closure.
                    continue;
                }

                let kind = new_regions_classification
                    .entry(*r)
                    .or_insert(CallRegionKind::Intersection(HashSet::new()));

                let CallRegionKind::Intersection(s) = kind else {
                    unreachable!();
                };

                s.insert(*r1);
            }
        }
        info!("call arg classification: {:?}", new_regions_classification);

        Ok(CallRegions {
            early_regions,
            late_regions,
            classification: new_regions_classification,
        })
    }

    fn translate_function_call(
        &mut self,
        func: &Operand<'tcx>,
        args: &[Operand<'tcx>],
        destination: &Place<'tcx>,
        target: Option<middle::mir::BasicBlock>,
        loc: Location,
        dying_loans: &[facts::Loan],
    ) -> Result<radium::Stmt, TranslationError> {
        let startpoint = self.info.interner.get_point_index(&facts::Point {
            location: loc,
            typ: facts::PointType::Start,
        });

        let Operand::Constant(box func_constant) = func else {
            return Err(TranslationError::UnsupportedFeature {
                description: format!(
                    "RefinedRust does currently not support this kind of call operand (got: {:?})",
                    func
                ),
            });
        };

        // for lifetime annotations:
        // 1. get the regions involved here. for that, get the instantiation of the function.
        //    + if it's a FnDef type, that should be easy.
        //    + for a function pointer: ?
        //    + for a closure: ?
        //   (Polonius does not seem to distinguish early/late bound in any way, except
        //   that they are numbered in different passes)
        // 2. find the constraints here involving the regions.
        // 3. solve for the regions.
        //    + transitively propagate the constraints
        //    + check for equalities
        //    + otherwise compute intersection. singleton intersection just becomes an equality def.
        // 4. add annotations accordingly
        //    + either a startlft
        //    + or a copy name
        // 5. add shortenlft annotations to line up arguments.
        //    + for that, we need the type of the LHS, and what the argument types (with
        //    substituted regions) should be.
        // 6. annotate the return value on assignment and establish constraints.

        let classification = self.compute_call_regions(func_constant, loc)?;

        // update the inclusion tracker with the new regions we have introduced
        // We just add the inclusions and ignore that we resolve it in a "tight" way.
        // the cases where we need the reverse inclusion should be really rare.
        for (r, c) in &classification.classification {
            match c {
                CallRegionKind::EqR(r2) => {
                    // put it at the start point, because the inclusions come into effect
                    // at the point right before.
                    self.inclusion_tracker.add_static_inclusion(*r, *r2, startpoint);
                    self.inclusion_tracker.add_static_inclusion(*r2, *r, startpoint);
                },
                CallRegionKind::Intersection(lfts) => {
                    // all the regions represented by lfts need to be included in r
                    for r2 in lfts {
                        self.inclusion_tracker.add_static_inclusion(*r2, *r, startpoint);
                    }
                },
            }
        }

        let func_expr = self.translate_operand(func, false)?;
        // translate the arguments
        let mut translated_args = Vec::new();
        for arg in args {
            // to_ty is the type the function expects

            //let ty = arg.ty(&self.proc.get_mir().local_decls, self.env.tcx());
            let translated_arg = self.translate_operand(arg, true)?;
            translated_args.push(translated_arg);
        }

        // make the call lifetime instantiation list
        let mut lifetime_insts = Vec::new();
        for early in &classification.early_regions {
            let lft = self.format_region(*early);
            lifetime_insts.push(lft);
        }
        for late in &classification.late_regions {
            let lft = self.format_region(*late);
            lifetime_insts.push(lft);
        }
        info!("Call lifetime instantiation: {:?}", lifetime_insts);

        // TODO: add annotations for the assignment
        // for that:
        // - get the type of the place
        // - enforce constraints as necessary. this might spawn dyninclusions with some of the new regions =>
        //   In Coq, also the aliases should get proper endlft events to resolve the dyninclusions.
        // - update the name map
        let call_expr = radium::Expr::Call {
            f: Box::new(func_expr),
            lfts: lifetime_insts,
            args: translated_args,
        };
        let stmt = match target {
            Some(target) => {
                let mut cont_stmt = self.translate_goto_like(&loc, target)?;
                // end loans before the goto, but after the call.
                // TODO: may cause duplications?
                cont_stmt = self.prepend_endlfts(cont_stmt, dying_loans.iter().copied());

                // Get the type of the return value from the function
                let (_, _, _, inst_sig) = self.call_expr_op_split_inst(func_constant)?;
                // TODO: do we need to do something with late bounds?
                let output_ty = inst_sig.output().skip_binder();
                info!("call has instantiated type {:?}", inst_sig);

                // compute the resulting annotations
                let (rhs_annots, pre_stmt_annots, post_stmt_annots) =
                    self.get_assignment_annots(loc, destination, output_ty);
                info!(
                    "assignment annots after call: expr: {:?}, pre-stmt: {:?}, post-stmt: {:?}",
                    rhs_annots, pre_stmt_annots, post_stmt_annots
                );

                let cont_stmt = radium::Stmt::with_annotations(
                    cont_stmt,
                    post_stmt_annots,
                    &Some("post_function_call".to_owned()),
                );

                // assign stmt with call; then jump to bb
                let place_ty = self.get_type_of_place(destination);
                let place_st = self.ty_translator.translate_type_to_syn_type(place_ty.ty)?;
                let place_expr = self.translate_place(destination)?;

                let ot = self.ty_translator.translate_syn_type_to_op_type(&place_st);

                let annotated_rhs = radium::Expr::with_optional_annotation(
                    call_expr,
                    rhs_annots,
                    Some("function_call".to_owned()),
                );
                let assign_stmt = radium::Stmt::Assign {
                    ot,
                    e1: place_expr,
                    e2: annotated_rhs,
                    s: Box::new(cont_stmt),
                };
                radium::Stmt::with_annotations(
                    assign_stmt,
                    pre_stmt_annots,
                    &Some("function_call".to_owned()),
                )
            },
            None => {
                // expr stmt with call; then stuck (we have not provided a continuation, after all)
                radium::Stmt::ExprS {
                    e: call_expr,
                    s: Box::new(radium::Stmt::Stuck),
                }
            },
        };

        let mut stmt_annots = Vec::new();

        // add annotations to initialize the regions for the call (before the call)
        for (r, class) in &classification.classification {
            let lft = self.format_region(*r);
            match class {
                CallRegionKind::EqR(r2) => {
                    let lft2 = self.format_region(*r2);
                    stmt_annots.push(radium::Annotation::CopyLftName(lft2, lft));
                },

                CallRegionKind::Intersection(rs) => {
                    match rs.len() {
                        0 => {
                            return Err(TranslationError::UnsupportedFeature {
                                description: "RefinedRust does currently not support unconstrained lifetime"
                                    .to_owned(),
                            });
                        },
                        1 => {
                            // this is really just an equality constraint
                            if let Some(r2) = rs.iter().next() {
                                let lft2 = self.format_region(*r2);
                                stmt_annots.push(radium::Annotation::CopyLftName(lft2, lft));
                            }
                        },
                        _ => {
                            // a proper intersection
                            let lfts: Vec<_> = rs.iter().map(|r| self.format_region(*r)).collect();
                            stmt_annots.push(radium::Annotation::AliasLftIntersection(lft, lfts));
                        },
                    };
                },
            }
        }

        let stmt = radium::Stmt::with_annotations(stmt, stmt_annots, &Some("function_call".to_owned()));
        Ok(stmt)
    }

    /// Translate a terminator.
    /// We pass the dying loans during this terminator. They need to be added at the right
    /// intermediate point.
    fn translate_terminator(
        &mut self,
        term: &Terminator<'tcx>,
        loc: Location,
        dying_loans: Vec<facts::Loan>,
    ) -> Result<radium::Stmt, TranslationError> {
        match &term.kind {
            TerminatorKind::Goto { target } => self.translate_goto_like(&loc, *target),

            TerminatorKind::Call {
                func,
                args,
                destination,
                target,
                ..
            } => {
                trace!("translating Call {:?}", term);
                if self.is_call_destination_panic(func) {
                    info!("Replacing call to std::panicking::begin_panic with Stuck");
                    return Ok(radium::Stmt::Stuck);
                }

                self.translate_function_call(func, args, destination, *target, loc, dying_loans.as_slice())
            },

            TerminatorKind::Return => {
                // TODO: this requires additional handling for reborrows

                // read from the return place
                // Is this semantics accurate wrt what the intended MIR semantics is?
                // Possibly handle this differently by making the first argument of a function a dedicated
                // return place? See also discussion at https://github.com/rust-lang/rust/issues/71117
                let stmt = radium::Stmt::Return(radium::Expr::Use {
                    ot: self.ty_translator.translate_syn_type_to_op_type(&self.return_synty),
                    e: Box::new(radium::Expr::Var(self.return_name.clone())),
                });

                // TODO is this right?
                Ok(self.prepend_endlfts(stmt, dying_loans.into_iter()))
            },

            //TerminatorKind::Abort => {
            //res_stmt = radium::Stmt::Stuck;
            //res_stmt = self.prepend_endlfts(res_stmt, dying_loans.into_iter());
            //},
            TerminatorKind::SwitchInt { discr, targets } => {
                let operand = self.translate_operand(discr, true)?;
                let all_targets: &[BasicBlock] = targets.all_targets();

                if self.get_type_of_operand(discr).is_bool() {
                    // we currently special-case this as Caesium has a built-in if and this is more
                    // convenient to handle for the type-checker

                    // implementation detail: the first index is the `false` branch, the second the
                    // `true` branch
                    let true_target = all_targets[1];
                    let false_target = all_targets[0];

                    let true_branch = self.translate_goto_like(&loc, true_target)?;
                    let false_branch = self.translate_goto_like(&loc, false_target)?;

                    let stmt = radium::Stmt::If {
                        e: operand,
                        ot: radium::OpType::Bool,
                        s1: Box::new(true_branch),
                        s2: Box::new(false_branch),
                    };

                    // TODO: is this right?
                    return Ok(self.prepend_endlfts(stmt, dying_loans.into_iter()));
                }

                //info!("switchint: {:?}", term.kind);
                let operand = self.translate_operand(discr, true)?;
                let ty = self.get_type_of_operand(discr);

                let mut target_map: HashMap<u128, usize> = HashMap::new();
                let mut translated_targets: Vec<radium::Stmt> = Vec::new();

                for (idx, (tgt, bb)) in targets.iter().enumerate() {
                    let bb: BasicBlock = bb;
                    let translated_target = self.translate_goto_like(&loc, bb)?;

                    target_map.insert(tgt, idx);
                    translated_targets.push(translated_target);
                }

                let translated_default = self.translate_goto_like(&loc, targets.otherwise())?;
                // TODO: need to put endlfts infront of gotos?

                let translated_ty = self.ty_translator.translate_type(ty)?;
                let radium::Type::Int(it) = translated_ty else {
                    return Err(TranslationError::UnknownError(
                        "SwitchInt switching on non-integer type".to_owned(),
                    ));
                };

                Ok(radium::Stmt::Switch {
                    e: operand,
                    it,
                    index_map: target_map,
                    bs: translated_targets,
                    def: Box::new(translated_default),
                })
            },

            TerminatorKind::Assert {
                cond,
                expected,
                target,
                ..
            } => {
                // this translation gets stuck on failure
                let cond_translated = self.translate_operand(cond, true)?;
                let comp = radium::Expr::BinOp {
                    o: radium::Binop::Eq,
                    ot1: radium::OpType::Bool,
                    ot2: radium::OpType::Bool,
                    e1: Box::new(cond_translated),
                    e2: Box::new(radium::Expr::Literal(radium::Literal::Bool(*expected))),
                };

                let stmt = self.translate_goto_like(&loc, *target)?;

                // TODO: should we really have this?
                let stmt = self.prepend_endlfts(stmt, dying_loans.into_iter());

                Ok(radium::Stmt::AssertS {
                    e: comp,
                    s: Box::new(stmt),
                })
            },

            TerminatorKind::Drop { place, target, .. } => {
                let ty = self.get_type_of_place(place);
                self.register_drop_shim_for(ty.ty);

                let place_translated = self.translate_place(place)?;
                let _drope = radium::Expr::DropE(Box::new(place_translated));

                let stmt = self.translate_goto_like(&loc, *target)?;

                Ok(self.prepend_endlfts(stmt, dying_loans.into_iter()))

                //res_stmt = radium::Stmt::ExprS { e: drope, s: Box::new(res_stmt)};
            },

            // just a goto for our purposes
            TerminatorKind::FalseEdge { real_target, .. }
            // this is just a virtual edge for the borrowchecker, we can translate this to a normal goto
            | TerminatorKind::FalseUnwind { real_target, .. } => {
                self.translate_goto_like(&loc, *real_target)
            },

            TerminatorKind::Unreachable => Ok(radium::Stmt::Stuck),

            TerminatorKind::UnwindResume => Err(TranslationError::Unimplemented {
                description: "implement UnwindResume".to_owned(),
            }),

            TerminatorKind::UnwindTerminate(_) => Err(TranslationError::Unimplemented {
                description: "implement UnwindTerminate".to_owned(),
            }),

            TerminatorKind::GeneratorDrop
            | TerminatorKind::InlineAsm { .. }
            | TerminatorKind::Yield { .. } => Err(TranslationError::UnsupportedFeature {
                description: format!(
                    "RefinedRust does currently not support this kind of terminator (got: {:?})",
                    term
                ),
            }),
        }
    }

    /// Prepend endlft annotations for dying loans to a statement.
    fn prepend_endlfts<I>(&self, st: radium::Stmt, dying: I) -> radium::Stmt
    where
        I: ExactSizeIterator<Item = facts::Loan>,
    {
        let mut cont_stmt = st;
        if dying.len() > 0 {
            //info!("Dying at {:?}: {:?}", loc, dying);
            for d in dying {
                let lft = self.info.atomic_region_of_loan(d);
                cont_stmt = radium::Stmt::Annot {
                    a: radium::Annotation::EndLft(self.format_atomic_region(&lft)),
                    s: Box::new(cont_stmt),
                    why: Some("endlft".to_owned()),
                };
            }
        }
        cont_stmt
    }

    /// Get predecessors in the CFG.
    fn get_loc_predecessors(&self, loc: Location) -> Vec<Location> {
        if loc.statement_index > 0 {
            let pred = Location {
                block: loc.block,
                statement_index: loc.statement_index - 1,
            };
            vec![pred]
        } else {
            // check for gotos that go to this basic block
            let pred_bbs = self.proc.predecessors(loc.block);
            let basic_blocks = &self.proc.get_mir().basic_blocks;
            pred_bbs
                .iter()
                .map(|bb| {
                    let data = &basic_blocks[*bb];
                    Location {
                        block: *bb,
                        statement_index: data.statements.len(),
                    }
                })
                .collect()
        }
    }

    /// Collect all the regions appearing in a type.
    fn find_region_variables_of_place_type(&self, ty: PlaceTy<'tcx>) -> Vec<Region> {
        let mut collector = TyRegionCollectFolder::new(self.env.tcx());
        if ty.variant_index.is_some() {
            panic!("find_region_variables_of_place_type: don't support enums");
        }

        ty.ty.fold_with(&mut collector);
        collector.get_regions()
    }

    /// Generate an annotation on an expression needed to update the region name map.
    fn generate_strong_update_annot(&self, ty: PlaceTy<'tcx>) -> Option<radium::Annotation> {
        let (interesting, tree) = self.generate_strong_update_annot_rec(ty.ty);
        interesting.then(|| radium::Annotation::GetLftNames(tree))
    }

    /// Returns a tree for giving names to Coq lifetimes based on RR types.
    /// The boolean indicates whether the tree is "interesting", i.e. whether it names at least one
    /// lifetime.
    fn generate_strong_update_annot_rec(&self, ty: Ty<'tcx>) -> (bool, radium::LftNameTree) {
        // TODO for now this just handles nested references
        match ty.kind() {
            ty::TyKind::Ref(r, ty, _) => match r.kind() {
                ty::RegionKind::ReVar(r) => {
                    let name = self.format_region(r);
                    let (_, ty_tree) = self.generate_strong_update_annot_rec(*ty);
                    (true, radium::LftNameTree::Ref(name, Box::new(ty_tree)))
                },
                _ => {
                    panic!("generate_strong_update_annot: expected region variable");
                },
            },
            _ => (false, radium::LftNameTree::Leaf),
        }
    }

    /// Generate an annotation to adapt the type of `expr` to `target_ty` from type `current_ty` by
    /// means of shortening lifetimes.
    fn generate_shortenlft_annot(
        &self,
        target_ty: Ty<'tcx>,
        _current_ty: Ty<'tcx>,
        mut expr: radium::Expr,
    ) -> radium::Expr {
        // this is not so different from the strong update annotation
        let (interesting, tree) = self.generate_strong_update_annot_rec(target_ty);
        if interesting {
            expr = radium::Expr::Annot {
                a: radium::Annotation::ShortenLft(tree),
                e: Box::new(expr),
                why: None,
            };
        }
        expr
    }

    /// Find all regions that need to outlive a loan region at its point of creation, and
    /// add the corresponding constraints to the inclusion tracker.
    fn get_outliving_regions_on_loan(&mut self, r: Region, loan_point: PointIndex) -> Vec<Region> {
        // get all base subset constraints r' ⊆ r
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let mut outliving = Vec::new();

        let subset_base = &input_facts.subset_base;
        for (r1, r2, p) in subset_base {
            if *p == loan_point && *r2 == r {
                self.inclusion_tracker.add_static_inclusion(*r1, *r2, *p);
                outliving.push(*r1);
            }
            // other subset constraints at this point are due to (for instance) the assignment of
            // the loan to a place and are handled there.
        }
        outliving
    }

    /// Check if a local is used for a spec closure.
    fn is_spec_closure_local(&self, l: Local) -> Result<Option<DefId>, TranslationError> {
        // check if we should ignore this
        let local_type = self.get_type_of_local(l)?;

        let TyKind::Closure(did, _) = local_type.kind() else {
            return Ok(None);
        };

        Ok(self
            .procedure_registry
            .lookup_function_mode(*did)
            .and_then(|m| m.is_ignore().then_some(*did)))
    }

    fn region_to_region_vid(r: ty::Region<'tcx>) -> facts::Region {
        match r.kind() {
            ty::RegionKind::ReVar(vid) => vid,
            _ => panic!(),
        }
    }

    /// Generate a dynamic inclusion of r1 in r2 at point p. Prepends annotations for doing so to `cont`.
    fn generate_dyn_inclusion(
        &mut self,
        stmt_annots: &mut Vec<radium::Annotation>,
        r1: Region,
        r2: Region,
        p: PointIndex,
    ) {
        // check if inclusion already holds
        if !self.inclusion_tracker.check_inclusion(r1, r2, p) {
            // check if the reverse inclusion already holds
            if self.inclusion_tracker.check_inclusion(r2, r1, p) {
                // our invariant is that this must be a static inclusion
                assert!(self.inclusion_tracker.check_static_inclusion(r2, r1, p));
                self.inclusion_tracker.add_dynamic_inclusion(r1, r2, p);

                // we generate an extendlft instruction
                // for this, we need to figure out a path to make this inclusion true, i.e. we need
                // an explanation of why it is syntactically included.
                // TODO: for now, we just assume that r1 ⊑ₗ [r2] (in terms of Coq lifetime inclusion)
                stmt_annots.push(radium::Annotation::ExtendLft(self.format_region(r1)));
            } else {
                self.inclusion_tracker.add_dynamic_inclusion(r1, r2, p);
                // we generate a dynamic inclusion instruction
                // we flip this around because the annotations are talking about lifetimes, which are oriented
                // the other way around.
                stmt_annots
                    .push(radium::Annotation::DynIncludeLft(self.format_region(r2), self.format_region(r1)));
            }
        }
    }

    /// Generates dynamic inclusions for the set of inclusions in `incls`.
    /// These inclusions should not hold yet.
    /// Skips mutual inclusions -- we cannot interpret these.
    fn generate_dyn_inclusions(
        &mut self,
        incls: &HashSet<(Region, Region, PointIndex)>,
    ) -> Vec<radium::Annotation> {
        // before executing the assignment, first enforce dynamic inclusions
        info!("Generating dynamic inclusions {:?}", incls);
        let mut stmt_annots = Vec::new();

        for (r1, r2, p) in incls {
            if incls.contains(&(*r2, *r1, *p)) {
                warn!("Skipping impossible dynamic inclusion {:?} ⊑ {:?} at {:?}", r1, r2, p);
                continue;
            }

            self.generate_dyn_inclusion(&mut stmt_annots, *r1, *r2, *p);
        }

        stmt_annots
    }

    /// Get the annotations due to borrows appearing on the RHS of an assignment.
    fn get_assignment_loan_annots(&mut self, loc: Location, rhs: &Rvalue<'tcx>) -> Vec<radium::Annotation> {
        let mut stmt_annots = Vec::new();

        // if we create a new loan here, start a new lifetime for it
        let loan_point = self.info.get_point(loc, facts::PointType::Mid);
        if let Some(loan) = self.info.get_optional_loan_at_location(loc) {
            // TODO: is this fine for aggregates? I suppose, if I create a loan for an
            // aggregate, I want to use the same atomic region for all of its components
            // anyways.

            let lft = self.info.atomic_region_of_loan(loan);
            let r = lft.get_region();

            // get the static inclusions we need to generate here and add them to the
            // inclusion tracker
            let outliving = self.get_outliving_regions_on_loan(r, loan_point);

            // add statement for issuing the loan
            stmt_annots.insert(
                0,
                radium::Annotation::StartLft(
                    self.format_atomic_region(&lft),
                    outliving.iter().map(|r| self.format_region(*r)).collect(),
                ),
            );

            let a = self.info.get_region_kind(r);
            info!("Issuing loan at {:?} with kind {:?}: {:?}; outliving: {:?}", loc, a, loan, outliving);
        } else if let Rvalue::Ref(region, BorrowKind::Shared, _) = rhs {
            // for shared reborrows, Polonius does not create a new loan, and so the
            // previous case did not match.
            // However, we still need to track the region created for the reborrow in an
            // annotation.

            let region = BodyTranslator::region_to_region_vid(*region);

            // find inclusion ?r1 ⊑ region -- we will actually enforce region = r1
            let new_constrs: Vec<(facts::Region, facts::Region)> =
                self.info.get_new_subset_constraints_at_point(loan_point);
            info!("Shared reborrow at {:?} with new constrs: {:?}", region, new_constrs);
            let mut included_region = None;
            for (r1, r2) in &new_constrs {
                if *r2 == region {
                    included_region = Some(r1);
                    break;
                }
            }
            if let Some(r) = included_region {
                //info!("Found inclusion {:?}⊑  {:?}", r, region);
                stmt_annots.push(radium::Annotation::CopyLftName(
                    self.format_region(*r),
                    self.format_region(region),
                ));

                // also add this to the inclusion checker
                self.inclusion_tracker.add_static_inclusion(*r, region, loan_point);
            } else {
                // This happens e.g. when borrowing from a raw pointer etc.
                info!("Found unconstrained shared borrow for {:?}", region);
                let inferred_constrained = vec![];

                // add statement for issuing the loan
                stmt_annots
                    .push(radium::Annotation::StartLft(self.format_region(region), inferred_constrained));
            }
        }

        stmt_annots
    }

    /// Compute the annotations for an assignment: an annotation for the rhs value, and a list of
    /// annotations to prepend to the statement, and a list of annotations to put after the
    /// statement.
    fn get_assignment_annots(
        &mut self,
        loc: Location,
        lhs: &Place<'tcx>,
        _rhs_ty: Ty<'tcx>,
    ) -> (Option<radium::Annotation>, Vec<radium::Annotation>, Vec<radium::Annotation>) {
        // check if the place is strongly writeable
        let strongly_writeable = !self.check_place_below_reference(lhs);
        let plc_ty = self.get_type_of_place(lhs);

        let new_dyn_inclusions;
        let expr_annot;
        let stmt_annot;
        if strongly_writeable {
            // we are going to update the region mapping through annotations,
            // and hence put up a barrier for propagation of region constraints

            // structurally go over the type and find region variables.
            // for each of the variables, issue a barrier.
            // also track them together with the PlaceItems needed to reach them.
            // from the latter, we can generate the necessary annotations
            let regions = self.find_region_variables_of_place_type(plc_ty);

            // put up a barrier at the Mid point
            let barrier_point_index = self.info.interner.get_point_index(&facts::Point {
                location: loc,
                typ: facts::PointType::Mid,
            });
            for r in &regions {
                self.inclusion_tracker.add_barrier(*r, barrier_point_index);
            }
            // get new constraints that should be enforced
            let new_constraints = self.get_assignment_strong_update_constraints(loc);
            stmt_annot = Vec::new();
            for (r1, r2, p) in &new_constraints {
                self.inclusion_tracker.add_static_inclusion(*r1, *r2, *p);
                self.inclusion_tracker.add_static_inclusion(*r2, *r1, *p);

                // TODO: use this instead of the expr_annot below
                //stmt_annot.push(
                //radium::Annotation::CopyLftName(
                //self.format_region(*r1),
                //self.format_region(*r2),
                //));
            }

            // TODO: get rid of this
            // similarly generate an annotation that encodes these constraints in the RR
            // type system
            expr_annot = self.generate_strong_update_annot(plc_ty);
            //expr_annot = None;

            new_dyn_inclusions = HashSet::new();
        } else {
            // need to filter out the constraints that are relevant here.
            // incrementally go through them.
            new_dyn_inclusions = self.get_assignment_weak_update_constraints(loc);
            expr_annot = None;
            stmt_annot = Vec::new();
        }

        // First enforce the new inclusions, then do the other annotations
        let new_dyn_inclusions = self.generate_dyn_inclusions(&new_dyn_inclusions);
        (expr_annot, new_dyn_inclusions, stmt_annot)
    }

    /// Get the regions appearing in a type.
    fn get_regions_of_ty(&self, ty: Ty<'tcx>) -> HashSet<ty::RegionVid> {
        let mut regions = HashSet::new();
        let mut clos = |r: ty::Region<'tcx>, _| match r.kind() {
            ty::RegionKind::ReVar(rv) => {
                regions.insert(rv);
                r
            },
            _ => r,
        };
        let mut folder = ty::fold::RegionFolder::new(self.env.tcx(), &mut clos);
        folder.fold_ty(ty);
        regions
    }

    /// On creating a composite value (e.g. a struct or enum), the composite value gets its own
    /// Polonius regions. We need to map these regions properly to the respective lifetimes.
    fn get_composite_rvalue_creation_annots(
        &mut self,
        loc: Location,
        rhs_ty: ty::Ty<'tcx>,
    ) -> Vec<radium::Annotation> {
        let info = &self.info;
        let input_facts = &info.borrowck_in_facts;
        let subset_base = &input_facts.subset_base;

        let regions_of_ty = self.get_regions_of_ty(rhs_ty);

        let mut annots = Vec::new();

        // Polonius subset constraint are spawned for the midpoint
        let midpoint = self.info.interner.get_point_index(&facts::Point {
            location: loc,
            typ: facts::PointType::Mid,
        });

        for (s1, s2, point) in subset_base {
            if *point == midpoint {
                let lft1 = self.info.mk_atomic_region(*s1);
                let lft2 = self.info.mk_atomic_region(*s2);

                // a place lifetime is included in a value lifetime
                if lft2.is_value() && lft1.is_place() {
                    // make sure it's not due to an assignment constraint
                    if regions_of_ty.contains(s2) && !subset_base.contains(&(*s2, *s1, midpoint)) {
                        // we enforce this inclusion by setting the lifetimes to be equal
                        self.inclusion_tracker.add_static_inclusion(*s1, *s2, midpoint);
                        self.inclusion_tracker.add_static_inclusion(*s2, *s1, midpoint);

                        let annot = radium::Annotation::CopyLftName(
                            self.format_atomic_region(&lft1),
                            self.format_atomic_region(&lft2),
                        );
                        annots.push(annot);
                    }
                }
            }
        }
        annots
    }

    /**
     * Translate a single basic block.
     */
    fn translate_basic_block(
        &mut self,
        bb_idx: BasicBlock,
        bb: &BasicBlockData<'tcx>,
    ) -> Result<radium::Stmt, TranslationError> {
        // we translate from back to front, starting with the terminator, since Caesium statements
        // have a continuation (the next statement to execute)

        // first do the endlfts for the things right before the terminator
        let mut idx = bb.statements.len();
        let loc = Location {
            block: bb_idx,
            statement_index: idx,
        };
        let dying = self.info.get_dying_loans(loc);
        // TODO zombie?
        let _dying_zombie = self.info.get_dying_zombie_loans(loc);
        let mut cont_stmt: radium::Stmt = self.translate_terminator(bb.terminator(), loc, dying)?;

        //cont_stmt = self.prepend_endlfts(cont_stmt, loc, dying);
        //cont_stmt = self.prepend_endlfts(cont_stmt, loc, dying_zombie);

        for stmt in bb.statements.iter().rev() {
            idx -= 1;
            let loc = Location {
                block: bb_idx,
                statement_index: idx,
            };

            // get all dying loans, and emit endlfts for these.
            // We loop over all predecessor locations, since some loans may end at the start of a
            // basic block (in particular related to NLL stuff)
            let pred = self.get_loc_predecessors(loc);
            let mut dying_loans = HashSet::new();
            for p in pred {
                let dying_between = self.info.get_loans_dying_between(p, loc, false);
                for l in &dying_between {
                    dying_loans.insert(*l);
                }
                // also include zombies
                let dying_between = self.info.get_loans_dying_between(p, loc, true);
                for l in &dying_between {
                    dying_loans.insert(*l);
                }
            }
            // we prepend them before the current statement

            match &stmt.kind {
                StatementKind::Assign(b) => {
                    let (plc, val) = b.as_ref();

                    if (self.is_spec_closure_local(plc.local)?).is_some() {
                        info!("skipping assignment to spec closure local: {:?}", plc);
                    } else if let Some(rewritten_ty) = self.checked_op_temporaries.get(&plc.local) {
                        // if this is a checked op, be sure to remember it
                        info!("rewriting assignment to checked op: {:?}", plc);

                        let synty = self.ty_translator.translate_type_to_syn_type(*rewritten_ty)?;

                        let translated_val = self.translate_rvalue(loc, val)?;
                        let translated_place = self.translate_place(plc)?;

                        // this should be a temporary
                        assert!(plc.projection.is_empty());

                        let ot = self.ty_translator.translate_syn_type_to_op_type(&synty);
                        cont_stmt = radium::Stmt::Assign {
                            ot,
                            e1: translated_place,
                            e2: translated_val,
                            s: Box::new(cont_stmt),
                        };
                    } else {
                        let plc_ty = self.get_type_of_place(plc);
                        let rhs_ty = val.ty(&self.proc.get_mir().local_decls, self.env.tcx());

                        let borrow_annots = self.get_assignment_loan_annots(loc, val);
                        let (expr_annot, pre_stmt_annots, post_stmt_annots) =
                            self.get_assignment_annots(loc, plc, rhs_ty);

                        // TODO; maybe move this to rvalue
                        let composite_annots = self.get_composite_rvalue_creation_annots(loc, rhs_ty);

                        cont_stmt = radium::Stmt::with_annotations(
                            cont_stmt,
                            post_stmt_annots,
                            &Some("post-assignment".to_owned()),
                        );

                        let translated_val = radium::Expr::with_optional_annotation(
                            self.translate_rvalue(loc, val)?,
                            expr_annot,
                            Some("assignment".to_owned()),
                        );
                        let translated_place = self.translate_place(plc)?;
                        let synty = self.ty_translator.translate_type_to_syn_type(plc_ty.ty)?;
                        let ot = self.ty_translator.translate_syn_type_to_op_type(&synty);
                        cont_stmt = radium::Stmt::Assign {
                            ot,
                            e1: translated_place,
                            e2: translated_val,
                            s: Box::new(cont_stmt),
                        };
                        cont_stmt = radium::Stmt::with_annotations(
                            cont_stmt,
                            pre_stmt_annots,
                            &Some("assignment".to_owned()),
                        );
                        cont_stmt = radium::Stmt::with_annotations(
                            cont_stmt,
                            borrow_annots,
                            &Some("borrow".to_owned()),
                        );
                        cont_stmt = radium::Stmt::with_annotations(
                            cont_stmt,
                            composite_annots,
                            &Some("composite".to_owned()),
                        );
                    }
                },

                StatementKind::Deinit(_) => {
                    // TODO: find out where this is emitted
                    return Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support Deinit".to_owned(),
                    });
                },

                StatementKind::FakeRead(b) => {
                    // we can probably ignore this, but I'm not sure
                    info!("Ignoring FakeRead: {:?}", b);
                },

                StatementKind::Intrinsic(_intrinsic) => {
                    return Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support Intrinsic".to_owned(),
                    });
                },

                StatementKind::PlaceMention(place) => {
                    // TODO: this is missed UB
                    info!("Ignoring PlaceMention: {:?}", place);
                },

                StatementKind::SetDiscriminant {
                    place: _place,
                    variant_index: _variant_index,
                } => {
                    // TODO
                    return Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support SetDiscriminant".to_owned(),
                    });
                },

                // don't need that info
                | StatementKind::AscribeUserType(_, _)
                // don't need that
                | StatementKind::Coverage(_)
                // no-op
                | StatementKind::ConstEvalCounter
                // ignore
                | StatementKind::Nop
                // just ignore
                | StatementKind::StorageLive(_)
                // just ignore
                | StatementKind::StorageDead(_)
                // just ignore retags
                | StatementKind::Retag(_, _) => (),
            }

            cont_stmt = self.prepend_endlfts(cont_stmt, dying_loans.into_iter());
        }

        Ok(cont_stmt)
    }

    /// Translate a `BorrowKind`.
    fn translate_borrow_kind(kind: BorrowKind) -> Result<radium::BorKind, TranslationError> {
        match kind {
            BorrowKind::Shared => Ok(radium::BorKind::Shared),
            BorrowKind::Shallow => {
                // TODO: figure out what to do with this
                // arises in match lowering
                Err(TranslationError::UnsupportedFeature {
                    description: "RefinedRust does currently not support shallow borrows".to_owned(),
                })
            },
            BorrowKind::Mut { .. } => {
                // TODO: handle two-phase borrows?
                Ok(radium::BorKind::Mutable)
            },
        }
    }

    const fn translate_mutability(mt: Mutability) -> radium::Mutability {
        match mt {
            Mutability::Mut => radium::Mutability::Mut,
            Mutability::Not => radium::Mutability::Shared,
        }
    }

    /// Get the inner type of a type to which we can apply the offset operator.
    fn get_offset_ty(ty: Ty<'tcx>) -> Result<Ty<'tcx>, TranslationError> {
        match ty.kind() {
            TyKind::Array(t, _) | TyKind::Slice(t) | TyKind::Ref(_, t, _) => Ok(*t),
            TyKind::RawPtr(tm) => Ok(tm.ty),
            _ => Err(TranslationError::UnknownError(format!("cannot take offset of {}", ty))),
        }
    }

    /// Translate binary operators.
    /// We need access to the operands, too, to handle the offset operator and get the right
    /// Caesium layout annotation.
    fn translate_binop(
        &self,
        op: BinOp,
        e1: &Operand<'tcx>,
        _e2: &Operand<'tcx>,
    ) -> Result<radium::Binop, TranslationError> {
        match op {
            BinOp::Add | BinOp::AddUnchecked => Ok(radium::Binop::Add),
            BinOp::Sub | BinOp::SubUnchecked => Ok(radium::Binop::Sub),
            BinOp::Mul | BinOp::MulUnchecked => Ok(radium::Binop::Mul),
            BinOp::Div => Ok(radium::Binop::Div),
            BinOp::Rem => Ok(radium::Binop::Mod),

            BinOp::BitXor => Ok(radium::Binop::BitXor),
            BinOp::BitAnd => Ok(radium::Binop::BitAnd),
            BinOp::BitOr => Ok(radium::Binop::BitOr),
            BinOp::Shl | BinOp::ShlUnchecked => Ok(radium::Binop::Shl),
            BinOp::Shr | BinOp::ShrUnchecked => Ok(radium::Binop::Shr),

            BinOp::Eq => Ok(radium::Binop::Eq),
            BinOp::Lt => Ok(radium::Binop::Lt),
            BinOp::Le => Ok(radium::Binop::Le),
            BinOp::Ne => Ok(radium::Binop::Ne),
            BinOp::Ge => Ok(radium::Binop::Ge),
            BinOp::Gt => Ok(radium::Binop::Gt),

            BinOp::Offset => {
                // we need to get the layout of the thing we're offsetting
                // try to get the type of e1.
                let e1_ty = self.get_type_of_operand(e1);
                let off_ty = BodyTranslator::get_offset_ty(e1_ty)?;
                let st = self.ty_translator.translate_type_to_syn_type(off_ty)?;
                let ly = self.ty_translator.translate_syn_type_to_layout(&st);
                Ok(radium::Binop::PtrOffset(ly))
            },
        }
    }

    /// Translate checked binary operators.
    /// We need access to the operands, too, to handle the offset operator and get the right
    /// Caesium layout annotation.
    fn translate_checked_binop(op: BinOp) -> Result<radium::Binop, TranslationError> {
        match op {
            BinOp::Add => Ok(radium::Binop::CheckedAdd),
            BinOp::Sub => Ok(radium::Binop::CheckedSub),
            BinOp::Mul => Ok(radium::Binop::CheckedMul),
            BinOp::Shl => Err(TranslationError::UnsupportedFeature {
                description: "RefinedRust does currently not support checked Shl".to_owned(),
            }),
            BinOp::Shr => Err(TranslationError::UnsupportedFeature {
                description: "RefinedRust does currently not support checked Shr".to_owned(),
            }),
            _ => Err(TranslationError::UnknownError(
                "unexpected checked binop that is not Add, Sub, Mul, Shl, or Shr".to_owned(),
            )),
        }
    }

    /// Translate unary operators.
    fn translate_unop(op: UnOp, ty: Ty<'tcx>) -> Result<radium::Unop, TranslationError> {
        match op {
            UnOp::Not => match ty.kind() {
                ty::TyKind::Bool => Ok(radium::Unop::NotBool),
                ty::TyKind::Int(_) | ty::TyKind::Uint(_) => Ok(radium::Unop::NotInt),
                _ => Err(TranslationError::UnknownError(
                    "application of UnOp::Not to non-{Int, Bool}".to_owned(),
                )),
            },
            UnOp::Neg => Ok(radium::Unop::Neg),
        }
    }

    /// Get the type to annotate a borrow with.
    fn get_type_annotation_for_borrow(
        &self,
        bk: BorrowKind,
        pl: &Place<'tcx>,
    ) -> Result<Option<radium::RustType>, TranslationError> {
        let BorrowKind::Mut { .. } = bk else {
            return Ok(None);
        };

        let ty = self.get_type_of_place(pl);

        // For borrows, we can safely ignore the downcast type -- we cannot borrow a particularly variant
        let translated_ty = self.ty_translator.translate_type(ty.ty)?;
        let annot_ty = radium::RustType::of_type(&translated_ty, &[]);

        Ok(Some(annot_ty))
    }

    /// Translates an Rvalue.
    fn translate_rvalue(
        &mut self,
        loc: Location,
        rval: &Rvalue<'tcx>,
    ) -> Result<radium::Expr, TranslationError> {
        match rval {
            Rvalue::Use(op) => {
                // converts an lvalue to an rvalue
                self.translate_operand(op, true)
            },

            Rvalue::Ref(region, bk, pl) => {
                let translated_pl = self.translate_place(pl)?;
                let translated_bk = BodyTranslator::translate_borrow_kind(*bk)?;
                let ty_annot = self.get_type_annotation_for_borrow(*bk, pl)?;

                if let Some(loan) = self.info.get_optional_loan_at_location(loc) {
                    let atomic_region = self.info.atomic_region_of_loan(loan);
                    let lft = self.format_atomic_region(&atomic_region);
                    Ok(radium::Expr::Borrow {
                        lft,
                        bk: translated_bk,
                        ty: ty_annot,
                        e: Box::new(translated_pl),
                    })
                } else {
                    info!("Didn't find loan at {:?}: {:?}; region {:?}", loc, rval, region);
                    let region = BodyTranslator::region_to_region_vid(*region);
                    let lft = self.format_region(region);

                    Ok(radium::Expr::Borrow {
                        lft,
                        bk: translated_bk,
                        ty: ty_annot,
                        e: Box::new(translated_pl),
                    })
                }
            },

            Rvalue::AddressOf(mt, pl) => {
                let translated_pl = self.translate_place(pl)?;
                let translated_mt = BodyTranslator::translate_mutability(*mt);

                Ok(radium::Expr::AddressOf {
                    mt: translated_mt,
                    e: Box::new(translated_pl),
                })
            },

            Rvalue::BinaryOp(op, operands) => {
                let e1 = &operands.as_ref().0;
                let e2 = &operands.as_ref().1;

                let e1_ty = self.get_type_of_operand(e1);
                let e2_ty = self.get_type_of_operand(e2);
                let e1_st = self.ty_translator.translate_type_to_syn_type(e1_ty)?;
                let e2_st = self.ty_translator.translate_type_to_syn_type(e2_ty)?;
                let e1_ot = self.ty_translator.translate_syn_type_to_op_type(&e1_st);
                let e2_ot = self.ty_translator.translate_syn_type_to_op_type(&e2_st);

                let translated_e1 = self.translate_operand(e1, true)?;
                let translated_e2 = self.translate_operand(e2, true)?;
                let translated_op = self.translate_binop(*op, &operands.as_ref().0, &operands.as_ref().1)?;

                Ok(radium::Expr::BinOp {
                    o: translated_op,
                    ot1: e1_ot,
                    ot2: e2_ot,
                    e1: Box::new(translated_e1),
                    e2: Box::new(translated_e2),
                })
            },

            Rvalue::CheckedBinaryOp(op, operands) => {
                let e1 = &operands.as_ref().0;
                let e2 = &operands.as_ref().1;

                let e1_ty = self.get_type_of_operand(e1);
                let e2_ty = self.get_type_of_operand(e2);
                let e1_st = self.ty_translator.translate_type_to_syn_type(e1_ty)?;
                let e2_st = self.ty_translator.translate_type_to_syn_type(e2_ty)?;
                let e1_ot = self.ty_translator.translate_syn_type_to_op_type(&e1_st);
                let e2_ot = self.ty_translator.translate_syn_type_to_op_type(&e2_st);

                let translated_e1 = self.translate_operand(e1, true)?;
                let translated_e2 = self.translate_operand(e2, true)?;
                let translated_op = BodyTranslator::translate_checked_binop(*op)?;

                Ok(radium::Expr::BinOp {
                    o: translated_op,
                    ot1: e1_ot,
                    ot2: e2_ot,
                    e1: Box::new(translated_e1),
                    e2: Box::new(translated_e2),
                })
            },

            Rvalue::UnaryOp(op, operand) => {
                let translated_e1 = self.translate_operand(operand, true)?;
                let e1_ty = self.get_type_of_operand(operand);
                let e1_st = self.ty_translator.translate_type_to_syn_type(e1_ty)?;
                let e1_ot = self.ty_translator.translate_syn_type_to_op_type(&e1_st);
                let translated_op = BodyTranslator::translate_unop(*op, e1_ty)?;

                Ok(radium::Expr::UnOp {
                    o: translated_op,
                    ot: e1_ot,
                    e: Box::new(translated_e1),
                })
            },

            Rvalue::NullaryOp(op, _ty) => {
                // TODO: SizeOf
                Err(TranslationError::UnsupportedFeature {
                    description: "RefinedRust does currently not support nullary ops (AlignOf, Sizeof)"
                        .to_owned(),
                })
            },

            Rvalue::Discriminant(pl) => {
                let ty = self.get_type_of_place(pl);
                let translated_pl = self.translate_place(pl)?;
                info!("getting discriminant of {:?} at type {:?}", pl, ty);

                let ty::TyKind::Adt(adt_def, args) = ty.ty.kind() else {
                    return Err(TranslationError::UnsupportedFeature {
                        description: format!(
                            "RefinedRust does currently not support discriminant accesses on non-enum types ({:?}, got {:?})",
                            rval, ty.ty
                        ),
                    });
                };

                let enum_use = self.ty_translator.generate_enum_use(*adt_def, args.iter())?;
                let els = enum_use.generate_raw_syn_type_term();

                let discriminant_acc = radium::Expr::EnumDiscriminant {
                    els: els.to_string(),
                    e: Box::new(translated_pl),
                };

                // need to do a load from this place
                let it = ty.ty.discriminant_ty(self.env.tcx());
                let translated_it = self.ty_translator.translate_type(it)?;

                let radium::Type::Int(translated_it) = translated_it else {
                    return Err(TranslationError::UnknownError(format!(
                        "type of discriminant is not an integer type {:?}",
                        it
                    )));
                };

                let ot = radium::OpType::Int(translated_it);

                Ok(radium::Expr::Use {
                    ot,
                    e: Box::new(discriminant_acc),
                })
            },

            Rvalue::Aggregate(kind, op) => {
                // translate operands
                let mut translated_ops: Vec<radium::Expr> = Vec::new();
                let mut operand_types: Vec<Ty<'tcx>> = Vec::new();

                for o in op {
                    let translated_o = self.translate_operand(o, true)?;
                    let type_of_o = self.get_type_of_operand(o);
                    translated_ops.push(translated_o);
                    operand_types.push(type_of_o);
                }

                match *kind {
                    box mir::AggregateKind::Tuple => {
                        if operand_types.is_empty() {
                            // translate to unit literal
                            return Ok(radium::Expr::Literal(radium::Literal::ZST));
                        }

                        let struct_use =
                            self.ty_translator.generate_tuple_use(operand_types.iter().copied())?;
                        let sl = struct_use.generate_raw_syn_type_term();
                        let initializers: Vec<_> =
                            translated_ops.into_iter().enumerate().map(|(i, o)| (i.to_string(), o)).collect();

                        Ok(radium::Expr::StructInitE {
                            sls: coq::AppTerm::new_lhs(sl.to_string()),
                            components: initializers,
                        })
                    },

                    box mir::AggregateKind::Adt(did, variant, args, ..) => {
                        // get the adt def
                        let adt_def: ty::AdtDef<'tcx> = self.env.tcx().adt_def(did);

                        if adt_def.is_struct() {
                            let variant = adt_def.variant(variant);
                            let struct_use = self.ty_translator.generate_struct_use(variant.def_id, args)?;

                            let Some(struct_use) = struct_use else {
                                // if not, it's replaced by unit
                                return Ok(radium::Expr::Literal(radium::Literal::ZST));
                            };

                            let sl = struct_use.generate_raw_syn_type_term();
                            let initializers: Vec<_> = translated_ops
                                .into_iter()
                                .zip(variant.fields.iter())
                                .map(|(o, field)| (field.name.to_string(), o))
                                .collect();

                            return Ok(radium::Expr::StructInitE {
                                sls: coq::AppTerm::new_lhs(sl.to_string()),
                                components: initializers,
                            });
                        }

                        if adt_def.is_enum() {
                            let variant_def = adt_def.variant(variant);

                            let struct_use =
                                self.ty_translator.generate_enum_variant_use(variant_def.def_id, args)?;
                            let sl = struct_use.generate_raw_syn_type_term();

                            let initializers: Vec<_> = translated_ops
                                .into_iter()
                                .zip(variant_def.fields.iter())
                                .map(|(o, field)| (field.name.to_string(), o))
                                .collect();

                            let variant_e = radium::Expr::StructInitE {
                                sls: coq::AppTerm::new_lhs(sl.to_string()),
                                components: initializers,
                            };

                            let enum_use = self.ty_translator.generate_enum_use(adt_def, args)?;
                            let els = enum_use.generate_raw_syn_type_term();

                            info!("generating enum annotation for type {:?}", enum_use);
                            let ty = radium::RustType::of_type(&radium::Type::Literal(enum_use), &[]);
                            let variant_name = variant_def.name.to_string();

                            return Ok(radium::Expr::EnumInitE {
                                els: coq::AppTerm::new_lhs(els.to_string()),
                                variant: variant_name,
                                ty,
                                initializer: Box::new(variant_e),
                            });
                        }

                        // TODO
                        Err(TranslationError::UnsupportedFeature {
                            description: format!(
                                "RefinedRust does currently not support aggregate rvalue for other ADTs (got: {:?})",
                                rval
                            ),
                        })
                    },
                    box mir::AggregateKind::Closure(def, _args) => {
                        trace!("Translating Closure aggregate value for {:?}", def);

                        // We basically translate this to a tuple
                        if operand_types.is_empty() {
                            // translate to unit literal
                            return Ok(radium::Expr::Literal(radium::Literal::ZST));
                        }

                        let struct_use =
                            self.ty_translator.generate_tuple_use(operand_types.iter().copied())?;
                        let sl = struct_use.generate_raw_syn_type_term();

                        let initializers: Vec<_> =
                            translated_ops.into_iter().enumerate().map(|(i, o)| (i.to_string(), o)).collect();

                        Ok(radium::Expr::StructInitE {
                            sls: coq::AppTerm::new_lhs(sl.to_string()),
                            components: initializers,
                        })
                    },

                    _ => {
                        // TODO
                        Err(TranslationError::UnsupportedFeature {
                            description: format!(
                                "RefinedRust does currently not support this kind of aggregate rvalue (got: {:?})",
                                rval
                            ),
                        })
                    },
                }
            },

            Rvalue::Cast(kind, op, ty) => {
                let op_ty = self.get_type_of_operand(op);
                let translated_op = self.translate_operand(op, true)?;

                match kind {
                    mir::CastKind::PointerCoercion(x) => {
                        match x {
                            ty::adjustment::PointerCoercion::MutToConstPointer => {
                                // this is a NOP in our model
                                Ok(translated_op)
                            },

                            ty::adjustment::PointerCoercion::ArrayToPointer
                            | ty::adjustment::PointerCoercion::ClosureFnPointer(_)
                            | ty::adjustment::PointerCoercion::ReifyFnPointer
                            | ty::adjustment::PointerCoercion::UnsafeFnPointer
                            | ty::adjustment::PointerCoercion::Unsize => {
                                Err(TranslationError::UnsupportedFeature {
                                    description: format!(
                                        "RefinedRust does currently not support this kind of pointer coercion (got: {:?})",
                                        rval
                                    ),
                                })
                            },
                        }
                    },

                    mir::CastKind::DynStar => Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support dyn* cast".to_owned(),
                    }),

                    mir::CastKind::IntToInt => {
                        // TODO
                        Err(TranslationError::Unimplemented {
                            description: "RefinedRust does currently not support int-to-int cast".to_owned(),
                        })
                    },

                    mir::CastKind::IntToFloat => Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support int-to-float cast".to_owned(),
                    }),

                    mir::CastKind::FloatToInt => Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support float-to-int cast".to_owned(),
                    }),

                    mir::CastKind::FloatToFloat => Err(TranslationError::UnsupportedFeature {
                        description: "RefinedRust does currently not support float-to-float cast".to_owned(),
                    }),

                    mir::CastKind::PtrToPtr => {
                        match (op_ty.kind(), ty.kind()) {
                            (TyKind::RawPtr(_), TyKind::RawPtr(_)) => {
                                // Casts between raw pointers are NOPs for us
                                Ok(translated_op)
                            },

                            _ => {
                                // TODO: any other cases we should handle?
                                Err(TranslationError::UnsupportedFeature {
                                    description: format!(
                                        "RefinedRust does currently not support ptr-to-ptr cast (got: {:?})",
                                        rval
                                    ),
                                })
                            },
                        }
                    },

                    mir::CastKind::FnPtrToPtr => Err(TranslationError::UnsupportedFeature {
                        description: format!(
                            "RefinedRust does currently not support fnptr-to-ptr cast (got: {:?})",
                            rval
                        ),
                    }),

                    mir::CastKind::Transmute => Err(TranslationError::UnsupportedFeature {
                        description: format!(
                            "RefinedRust does currently not support transmute cast (got: {:?})",
                            rval
                        ),
                    }),

                    mir::CastKind::PointerExposeAddress | mir::CastKind::PointerFromExposedAddress => {
                        Err(TranslationError::UnsupportedFeature {
                            description: format!(
                                "RefinedRust does currently not support this kind of cast (got: {:?})",
                                rval
                            ),
                        })
                    },
                }
            },

            Rvalue::CopyForDeref(_)
            | Rvalue::Len(..)
            | Rvalue::Repeat(..)
            | Rvalue::ThreadLocalRef(..)
            | Rvalue::ShallowInitBox(_, _) => Err(TranslationError::UnsupportedFeature {
                description: format!(
                    "RefinedRust does currently not support this kind of rvalue (got: {:?})",
                    rval
                ),
            }),
        }
    }

    /// Make a trivial place accessing `local`.
    fn make_local_place(&self, local: Local) -> Place<'tcx> {
        Place {
            local,
            projection: self.env.tcx().mk_place_elems(&[]),
        }
    }

    /// Translate an operand.
    /// This will either generate an lvalue (in case of Move or Copy) or an rvalue (in most cases
    /// of Constant). How this is used depends on the context. (e.g., Use of an integer constant
    /// does not typecheck, and produces a stuck program).
    fn translate_operand(
        &mut self,
        op: &Operand<'tcx>,
        to_rvalue: bool,
    ) -> Result<radium::Expr, TranslationError> {
        match op {
            // In Caesium: typed_place needs deref (not use) for place accesses.
            // use is used top-level to convert an lvalue to an rvalue, which is why we use it here.
            Operand::Copy(place) | Operand::Move(place) => {
                // check if this goes to a temporary of a checked op
                let place_kind = if self.checked_op_temporaries.contains_key(&place.local) {
                    assert!(place.projection.len() == 1);

                    let ProjectionElem::Field(f, _0) = place.projection[0] else {
                        unreachable!("invariant violation for access to checked op temporary");
                    };

                    if f.index() != 0 {
                        // make this a constant false -- our semantics directly checks for overflows
                        // and otherwise throws UB.
                        return Ok(radium::Expr::Literal(radium::Literal::Bool(false)));
                    }

                    // access to the result of the op
                    self.make_local_place(place.local)
                } else {
                    *place
                };

                let translated_place = self.translate_place(&place_kind)?;
                let ty = self.get_type_of_place(place);

                let st = self.ty_translator.translate_type_to_syn_type(ty.ty)?;
                let ot = self.ty_translator.translate_syn_type_to_op_type(&st);

                if to_rvalue {
                    Ok(radium::Expr::Use {
                        ot,
                        e: Box::new(translated_place),
                    })
                } else {
                    Ok(translated_place)
                }
            },
            Operand::Constant(constant) => {
                // TODO: possibly need different handling of the rvalue flag
                // when this also handles string literals etc.
                return self.translate_constant(constant.as_ref());
            },
        }
    }

    fn translate_fn_def_use(&mut self, ty: Ty<'tcx>) -> Result<radium::Expr, TranslationError> {
        let TyKind::FnDef(defid, params) = ty.kind() else {
            return Err(TranslationError::UnknownError("not a FnDef type".to_owned()));
        };

        let key: ty::ParamEnv<'tcx> = self.env.tcx().param_env(self.proc.get_id());
        if self.env.tcx().trait_of_item(*defid).is_none() {
            // track that we are using this function and generate the Coq location name
            let param_name = self.register_use_procedure(*defid, params)?;
            return Ok(radium::Expr::MetaParam(param_name));
        }

        let Some((resolved_did, resolved_params, kind)) =
            traits::resolve_assoc_item(self.env.tcx(), key, *defid, params)
        else {
            return Err(TranslationError::TraitResolution(format!("Could not resolve trait {:?}", defid)));
        };

        info!(
            "Resolved trait method {:?} as {:?} with substs {:?} and kind {:?}",
            defid, resolved_did, resolved_params, kind
        );

        match kind {
            traits::TraitResolutionKind::UserDefined => {
                let param_name = self.register_use_procedure(resolved_did, resolved_params)?;
                Ok(radium::Expr::MetaParam(param_name))
            },

            traits::TraitResolutionKind::Param => Err(TranslationError::Unimplemented {
                description: "Implement trait invocation for Param".to_owned(),
            }),

            traits::TraitResolutionKind::Closure => {
                // TODO: here, we should first generate an instance of the trait
                //self.env.tcx().
                // mir_shims(middle::ty::InstanceDef::Item(resolved_did));
                // the args are just the closure args. We can ignore them.
                let _clos_args = resolved_params.as_closure();
                let param_name = self.register_use_procedure(resolved_did, ty::List::empty())?;
                Ok(radium::Expr::MetaParam(param_name))
                //Err(TranslationError::Unimplemented { description: format!("Implement trait
                // invocation for Closure") })
            },
        }
    }

    /// Translate a scalar at a specific type to a `radium::Expr`.
    // TODO: Use `TryFrom` instead
    fn translate_scalar(&mut self, sc: &Scalar, ty: Ty<'tcx>) -> Result<radium::Expr, TranslationError> {
        // TODO: Use `TryFrom` instead
        fn translate_literal<T, U>(
            sc: Result<T, U>,
            fptr: fn(T) -> radium::Literal,
        ) -> Result<radium::Expr, TranslationError> {
            sc.map_or(Err(TranslationError::InvalidLayout), |lit| Ok(radium::Expr::Literal(fptr(lit))))
        }

        match ty.kind() {
            TyKind::Bool => translate_literal(sc.to_bool(), radium::Literal::Bool),

            TyKind::Int(it) => match it {
                ty::IntTy::I8 => translate_literal(sc.to_i8(), radium::Literal::I8),
                ty::IntTy::I16 => translate_literal(sc.to_i16(), radium::Literal::I16),
                ty::IntTy::I32 => translate_literal(sc.to_i32(), radium::Literal::I32),
                ty::IntTy::I128 => translate_literal(sc.to_i128(), radium::Literal::I128),

                // For Radium, the pointer size is 8 bytes
                ty::IntTy::I64 | ty::IntTy::Isize => translate_literal(sc.to_i64(), radium::Literal::I64),
            },

            TyKind::Uint(it) => match it {
                ty::UintTy::U8 => translate_literal(sc.to_u8(), radium::Literal::U8),
                ty::UintTy::U16 => translate_literal(sc.to_u16(), radium::Literal::U16),
                ty::UintTy::U32 => translate_literal(sc.to_u32(), radium::Literal::U32),
                ty::UintTy::U128 => translate_literal(sc.to_u128(), radium::Literal::U128),

                // For Radium, the pointer is 8 bytes
                ty::UintTy::U64 | ty::UintTy::Usize => translate_literal(sc.to_u64(), radium::Literal::U64),
            },

            TyKind::FnDef(_, _) => self.translate_fn_def_use(ty),

            TyKind::Tuple(tys) => {
                if tys.is_empty() {
                    return Ok(radium::Expr::Literal(radium::Literal::ZST));
                }

                Err(TranslationError::UnsupportedFeature {
                    description: format!(
                        "RefinedRust does currently not support compound construction of tuples using literals (got: {:?})",
                        ty
                    ),
                })
            },

            TyKind::Ref(_, _, _) => match sc {
                Scalar::Int(_) => unreachable!(),

                Scalar::Ptr(pointer, _) => {
                    let glob_alloc = self.env.tcx().global_alloc(pointer.provenance);
                    match glob_alloc {
                        middle::mir::interpret::GlobalAlloc::Static(did) => {
                            info!(
                                "Found static GlobalAlloc {:?} for Ref scalar {:?} at type {:?}",
                                did, sc, ty
                            );

                            let Some(s) = self.const_registry.statics.get(&did) else {
                                return Err(TranslationError::UnknownError(format!(
                                    "Did not find a registered static for GlobalAlloc {:?} for scalar {:?} at type {:?}; registered: {:?}",
                                    glob_alloc, sc, ty, self.const_registry.statics
                                )));
                            };

                            self.collected_statics.insert(did);
                            Ok(radium::Expr::Literal(radium::Literal::Loc(s.loc_name.clone())))
                        },

                        _ => Err(TranslationError::UnsupportedFeature {
                            description: format!(
                                "RefinedRust does currently not support GlobalAlloc {:?} for scalar {:?} at type {:?}",
                                glob_alloc, sc, ty
                            ),
                        }),
                    }
                },
            },

            _ => Err(TranslationError::UnsupportedFeature {
                description: format!(
                    "RefinedRust does currently not support layout for const value: (got: {:?})",
                    ty
                ),
            }),
        }
    }

    /// Translate a constant value from const evaluation.
    fn translate_constant_value(
        &mut self,
        v: mir::interpret::ConstValue<'tcx>,
        ty: Ty<'tcx>,
    ) -> Result<radium::Expr, TranslationError> {
        match v {
            ConstValue::Scalar(sc) => self.translate_scalar(&sc, ty),
            ConstValue::ZeroSized => {
                // TODO are there more special cases we need to handle somehow?
                match ty.kind() {
                    TyKind::FnDef(_, _) => {
                        info!("Translating ZST val for function call target: {:?}", ty);
                        self.translate_fn_def_use(ty)
                    },
                    _ => Ok(radium::Expr::Literal(radium::Literal::ZST)),
                }
            },
            _ => {
                // TODO: do we actually care about this case or is this just something that can
                // appear as part of CTFE/MIRI?
                Err(TranslationError::UnsupportedFeature {
                    description: format!("Unsupported Constant: ConstValue; {:?}", v),
                })
            },
        }
    }

    /// Translate a Constant to a `radium::Expr`.
    fn translate_constant(&mut self, constant: &Constant<'tcx>) -> Result<radium::Expr, TranslationError> {
        match constant.literal {
            ConstantKind::Ty(v) => {
                let const_ty = v.ty();

                match v.kind() {
                    ConstKind::Value(v) => {
                        // this doesn't contain the necessary structure anymore. Need to reconstruct using the
                        // type.
                        match v.try_to_scalar() {
                            Some(sc) => self.translate_scalar(&sc, const_ty),
                            _ => Err(TranslationError::UnsupportedFeature {
                                description: format!("const value not supported: {:?}", v),
                            }),
                        }
                    },
                    _ => Err(TranslationError::UnsupportedFeature {
                        description: "Unsupported ConstKind".to_owned(),
                    }),
                }
            },
            ConstantKind::Val(val, ty) => self.translate_constant_value(val, ty),
            ConstantKind::Unevaluated(c, ty) => {
                // call const evaluation
                let param_env: ty::ParamEnv<'tcx> = self.env.tcx().param_env(self.proc.get_id());
                match self.env.tcx().const_eval_resolve(param_env, c, None) {
                    Ok(res) => self.translate_constant_value(res, ty),
                    Err(e) => match e {
                        ErrorHandled::Reported(_) => Err(TranslationError::UnsupportedFeature {
                            description: "Cannot interpret constant".to_owned(),
                        }),
                        ErrorHandled::TooGeneric => Err(TranslationError::UnsupportedFeature {
                            description: "Const use is too generic".to_owned(),
                        }),
                    },
                }
            },
        }
    }

    /// Translate a place to a Caesium lvalue.
    fn translate_place(&mut self, pl: &Place<'tcx>) -> Result<radium::Expr, TranslationError> {
        // Get the type of the underlying local. We will use this to
        // get the necessary layout information for dereferencing
        let mut cur_ty = self.get_type_of_local(pl.local).map(PlaceTy::from_ty)?;

        let local_name = self
            .variable_map
            .get(&pl.local)
            .ok_or_else(|| TranslationError::UnknownVar(format!("{:?}", pl.local)))?;

        let mut acc_expr = radium::Expr::Var(local_name.to_string());

        // iterate in evaluation order
        for it in pl.projection {
            match &it {
                ProjectionElem::Deref => {
                    // use the type of the dereferencee
                    let st = self.ty_translator.translate_type_to_syn_type(cur_ty.ty)?;
                    let ot = self.ty_translator.translate_syn_type_to_op_type(&st);
                    acc_expr = radium::Expr::Deref {
                        ot,
                        e: Box::new(acc_expr),
                    };
                },
                ProjectionElem::Field(f, _) => {
                    // `t` is the type of the field we are accessing!
                    let lit = self.ty_translator.generate_structlike_use(cur_ty.ty, cur_ty.variant_index)?;
                    // TODO: does not do the right thing for accesses to fields of zero-sized objects.
                    let struct_sls = lit.map_or(radium::SynType::Unit, |x| x.generate_raw_syn_type_term());
                    let name = self.ty_translator.translator.get_field_name_of(
                        *f,
                        cur_ty.ty,
                        cur_ty.variant_index.map(abi::VariantIdx::as_usize),
                    )?;

                    acc_expr = radium::Expr::FieldOf {
                        e: Box::new(acc_expr),
                        name,
                        sls: struct_sls.to_string(),
                    };
                },
                ProjectionElem::Index(_v) => {
                    //TODO
                    return Err(TranslationError::UnsupportedFeature {
                        description: "places: implement index access".to_owned(),
                    });
                },
                ProjectionElem::ConstantIndex { .. } => {
                    //TODO
                    return Err(TranslationError::UnsupportedFeature {
                        description: "places: implement const index access".to_owned(),
                    });
                },
                ProjectionElem::Subslice { .. } => {
                    return Err(TranslationError::UnsupportedFeature {
                        description: "places: implement subslicing".to_owned(),
                    });
                },
                ProjectionElem::Downcast(_, variant_idx) => {
                    info!("Downcast of ty {:?} to {:?}", cur_ty, variant_idx);
                    if let ty::TyKind::Adt(def, args) = cur_ty.ty.kind() {
                        if def.is_enum() {
                            let enum_use = self.ty_translator.generate_enum_use(*def, args.iter())?;
                            let els = enum_use.generate_raw_syn_type_term();

                            let variant_name = TypeTranslator::get_variant_name_of(cur_ty.ty, *variant_idx)?;

                            acc_expr = radium::Expr::EnumData {
                                els: els.to_string(),
                                variant: variant_name,
                                e: Box::new(acc_expr),
                            }
                        } else {
                            return Err(TranslationError::UnknownError(
                                "places: ADT downcasting on non-enum type".to_owned(),
                            ));
                        }
                    } else {
                        return Err(TranslationError::UnknownError(
                            "places: ADT downcasting on non-enum type".to_owned(),
                        ));
                    }
                },
                ProjectionElem::OpaqueCast(_) => {
                    return Err(TranslationError::UnsupportedFeature {
                        description: "places: implement opaque casts".to_owned(),
                    });
                },
            };
            // update cur_ty
            cur_ty = cur_ty.projection_ty(self.env.tcx(), it);
        }
        info!("translating place {:?} to {:?}", pl, acc_expr);
        Ok(acc_expr)
    }

    /// Get the type of a local in a body.
    fn get_type_of_local(&self, local: Local) -> Result<Ty<'tcx>, TranslationError> {
        self.proc
            .get_mir()
            .local_decls
            .get(local)
            .map(|decl| decl.ty)
            .ok_or_else(|| TranslationError::UnknownVar(String::new()))
    }

    /// Get the type of a place expression.
    fn get_type_of_place(&self, pl: &Place<'tcx>) -> PlaceTy<'tcx> {
        pl.ty(&self.proc.get_mir().local_decls, self.env.tcx())
    }

    /// Get the type of a const.
    fn get_type_of_const(cst: &Constant<'tcx>) -> Ty<'tcx> {
        match cst.literal {
            ConstantKind::Ty(cst) => cst.ty(),
            ConstantKind::Val(_, ty) | ConstantKind::Unevaluated(_, ty) => ty,
        }
    }

    /// Get the type of an operand.
    fn get_type_of_operand(&self, op: &Operand<'tcx>) -> Ty<'tcx> {
        op.ty(&self.proc.get_mir().local_decls, self.env.tcx())
    }
}