use std::cmp::{max, min};
use std::collections::HashSet;
use std::iter::zip;

use hercules_ir::dataflow::*;
use hercules_ir::def_use::get_uses;
use hercules_ir::ir::*;

use crate::*;

/*
 * The ccp lattice tracks, for each node, the following information:
 * 1. Reachability - is it possible for this node to be reached during any
 *    execution?
 * 2. Constant - does this node evaluate to a constant expression?
 * The ccp lattice is formulated as a combination of consistuent lattices. The
 * flow function for the ccp dataflow analysis "crosses" information across the
 * sub lattices - for example, whether a condition is constant may inform
 * whether a branch target is reachable. This analysis uses interpreted
 * constants, so constant one plus constant one results in constant two.
 */
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CCPLattice {
    reachability: ReachabilityLattice,
    constant: ConstantLattice,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ReachabilityLattice {
    Unreachable,
    Reachable,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConstantLattice {
    Top,
    Constant(Constant),
    Bottom,
}

impl CCPLattice {
    fn is_reachable(&self) -> bool {
        self.reachability == ReachabilityLattice::Reachable
    }

    fn get_constant(&self) -> Option<Constant> {
        if let ConstantLattice::Constant(cons) = &self.constant {
            Some(cons.clone())
        } else {
            None
        }
    }
}

impl ReachabilityLattice {
    // We define join for the reachability lattice as for data nodes we join the reachability of
    // its uses since it is unreachable if any of its uses are unreachable
    fn join(a: &Self, b: &Self) -> Self {
        match (a, b) {
            (ReachabilityLattice::Reachable, ReachabilityLattice::Reachable) => {
                ReachabilityLattice::Reachable
            }
            _ => ReachabilityLattice::Unreachable,
        }
    }
}

impl ConstantLattice {
    fn is_top(&self) -> bool {
        *self == ConstantLattice::Top
    }

    fn is_bottom(&self) -> bool {
        *self == ConstantLattice::Bottom
    }
}

impl Semilattice for CCPLattice {
    fn meet(a: &Self, b: &Self) -> Self {
        CCPLattice {
            reachability: ReachabilityLattice::meet(&a.reachability, &b.reachability),
            constant: ConstantLattice::meet(&a.constant, &b.constant),
        }
    }

    fn bottom() -> Self {
        CCPLattice {
            reachability: ReachabilityLattice::bottom(),
            constant: ConstantLattice::bottom(),
        }
    }

    fn top() -> Self {
        CCPLattice {
            reachability: ReachabilityLattice::top(),
            constant: ConstantLattice::top(),
        }
    }
}

impl Semilattice for ReachabilityLattice {
    fn meet(a: &Self, b: &Self) -> Self {
        match (a, b) {
            (ReachabilityLattice::Unreachable, ReachabilityLattice::Unreachable) => {
                ReachabilityLattice::Unreachable
            }
            _ => ReachabilityLattice::Reachable,
        }
    }

    fn bottom() -> Self {
        ReachabilityLattice::Reachable
    }

    fn top() -> Self {
        ReachabilityLattice::Unreachable
    }
}

impl Semilattice for ConstantLattice {
    fn meet(a: &Self, b: &Self) -> Self {
        match (a, b) {
            (ConstantLattice::Top, b) => b.clone(),
            (a, ConstantLattice::Top) => a.clone(),
            (ConstantLattice::Constant(cons1), ConstantLattice::Constant(cons2)) => {
                if cons1 == cons2 {
                    ConstantLattice::Constant(cons1.clone())
                } else {
                    ConstantLattice::Bottom
                }
            }
            _ => ConstantLattice::Bottom,
        }
    }

    fn bottom() -> Self {
        ConstantLattice::Bottom
    }

    fn top() -> Self {
        ConstantLattice::Top
    }
}

/* Macros used for constant propagation with intrinsic functions */
macro_rules! unary_float_intrinsic {
    ($intrinsic : expr, $constants : expr, $func : ident) => {
        if let Constant::Float32(v) = $constants[0] {
            ConstantLattice::Constant(Constant::Float32(ordered_float::OrderedFloat(v.$func())))
        } else if let Constant::Float64(v) = $constants[0] {
            ConstantLattice::Constant(Constant::Float64(ordered_float::OrderedFloat(v.$func())))
        } else {
            panic!("Unsupported combination of intrinsic {} and constant value. Did typechecking succeed?",
                   $intrinsic.lower_case_name())
        }
    }
}

macro_rules! binary_float_intrinsic {
    ($intrinsic : expr, $constants : expr, $func : ident) => {
        if let (Constant::Float32(x), Constant::Float32(y))
                    = ($constants[0], $constants[1]) {
            ConstantLattice::Constant(Constant::Float32(ordered_float::OrderedFloat(x.$func(**y))))
        } else if let (Constant::Float64(x), Constant::Float64(y))
                    = ($constants[0], $constants[1]) {
            ConstantLattice::Constant(Constant::Float64(ordered_float::OrderedFloat(x.$func(**y))))
        } else {
            panic!("Unsupported combination of intrinsic {} and constant values. Did typechecking succeed?",
                   $intrinsic.lower_case_name())
        }
    }
}

/*
 * Top level function to run conditional constant propagation.
 */
pub fn ccp(editor: &mut FunctionEditor, reverse_postorder: &Vec<NodeID>) {
    // Step 1: run ccp analysis to understand the function.
    let result = dataflow_global(editor.func(), reverse_postorder, |inputs, node_id| {
        ccp_flow_function(inputs, node_id, editor)
    });

    // Check the results of CCP. In particular we assert that for every reachable node (except for
    // region and phi nodes) all of its uses are also reachable. For phi nodes we check this
    // property only on live branches and for regions we check that at least one of its
    // predecessors is reachable if it is reachable
    for node_idx in 0..result.len() {
        if !result[node_idx].is_reachable() {
            continue;
        }
        match &editor.func().nodes[node_idx] {
            Node::Region { preds } => {
                assert!(preds.iter().any(|n| result[n.idx()].is_reachable()))
            }
            Node::Phi { control, data } => {
                assert!(result[control.idx()].is_reachable());
                let region_preds =
                    if let Node::Region { preds } = &editor.func().nodes[control.idx()] {
                        preds
                    } else {
                        panic!("A phi's control input must be a region node.")
                    };
                assert!(zip(region_preds.iter(), data.iter()).all(|(region, data)| {
                    !result[region.idx()].is_reachable() || result[data.idx()].is_reachable()
                }));
            }
            _ => {
                assert!(get_uses(&editor.func().nodes[node_idx])
                    .as_ref()
                    .iter()
                    .all(|n| result[n.idx()].is_reachable()));
            }
        }
    }

    // Step 2: propagate constants. For each node that was found to have a constant value, we
    // create a node for that constant value, replace uses of the original node with the constant,
    // and finally delete the original node
    let mut unreachable: HashSet<NodeID> = HashSet::new();
    for (idx, res) in result.into_iter().enumerate() {
        let old_id = NodeID::new(idx);
        if let Some(cons) = res.get_constant() {
            assert!(!editor.func().nodes[idx].is_control());
            editor.edit(|mut edit| {
                // Get the ConstantID of this constant value.
                let cons_id = edit.add_constant(cons);
                // Add a constant IR node for this constant
                let cons_node = edit.add_node(Node::Constant { id: cons_id });
                // Replace the original node with the constant node
                edit.sub_edit(old_id, cons_node);
                edit = edit.replace_all_uses(old_id, cons_node)?;
                edit.delete_node(old_id)
            });
        }
        if !res.is_reachable() {
            unreachable.insert(old_id);
        }
    }

    // Step 3: delete dead branches. Any nodes that are unreachable should be
    // deleted. Any if or match nodes that now are light on users need to be
    // removed immediately, since if and match nodes have requirements on the
    // number of users.

    // Step 3.1: remove uses of data nodes in phi nodes corresponding to
    // unreachable uses in corresponding region nodes.
    for phi_id in (0..editor.func().nodes.len()).map(NodeID::new) {
        if unreachable.contains(&phi_id) {
            continue;
        }

        if let Node::Phi { control, data } = &editor.func().nodes[phi_id.idx()] {
            if let Node::Region { preds } = &editor.func().nodes[control.idx()] {
                let control = *control;
                let new_data = zip(preds.iter(), data.iter())
                    .filter(|(pred, _)| !unreachable.contains(pred))
                    .map(|(_, datum)| *datum)
                    .collect();
                editor.edit(|mut edit| {
                    let new_node = edit.add_node(Node::Phi {
                        control,
                        data: new_data,
                    });
                    edit.sub_edit(phi_id, new_node);
                    edit = edit.replace_all_uses(phi_id, new_node)?;
                    edit.delete_node(phi_id)
                });
            }
        }
    }

    // Step 3.2: remove uses of unreachable nodes in region nodes.
    for region_id in (0..editor.func().nodes.len()).map(NodeID::new) {
        if unreachable.contains(&region_id) {
            continue;
        }

        if let Node::Region { preds } = &editor.func().nodes[region_id.idx()] {
            let new_preds = preds
                .iter()
                .filter(|pred| !unreachable.contains(pred))
                .map(|x| *x)
                .collect();
            editor.edit(|mut edit| {
                let new_node = edit.add_node(Node::Region { preds: new_preds });
                edit.sub_edit(region_id, new_node);
                edit = edit.replace_all_uses(region_id, new_node)?;
                edit.delete_node(region_id)
            });
        }
    }

    // Step 3.3: remove if and match nodes with one reachable user.
    for branch_id in (0..editor.func().nodes.len()).map(NodeID::new) {
        if unreachable.contains(&branch_id) {
            continue;
        }

        if let Node::If { control, cond: _ } | Node::Match { control, sum: _ } =
            &editor.func().nodes[branch_id.idx()]
        {
            let control = *control;

            let users = editor.get_users(branch_id).collect::<Vec<NodeID>>();
            let reachable_users = users
                .iter()
                .map(|x| *x)
                .filter(|user| !unreachable.contains(user))
                .collect::<Vec<NodeID>>();
            let the_reachable_user = reachable_users[0];

            // The reachable users iterator will contain one user if we need to
            // remove this branch node.
            if reachable_users.len() == 1 {
                // The user is a projection node, which in turn has one user.
                editor.edit(|mut edit| {
                    // Replace all uses of the single reachable user with the node preceeding the
                    // branch node
                    edit = edit.replace_all_uses(the_reachable_user, control)?;
                    // Mark the reachable user and the branch as unreachable so they'll be deleted
                    unreachable.insert(the_reachable_user);
                    unreachable.insert(branch_id);
                    Ok(edit)
                });
            }
        }
    }

    // Step 3.4: delete unreachable nodes.
    editor.edit(|mut edit| {
        for node in unreachable {
            edit = edit.delete_node(node)?;
        }
        Ok(edit)
    });
}

fn ccp_flow_function(
    inputs: &[CCPLattice],
    node_id: NodeID,
    editor: &FunctionEditor,
) -> CCPLattice {
    let node = &editor.func().nodes[node_id.idx()];
    match node {
        Node::Start => CCPLattice::bottom(),
        Node::Region { preds } => preds.iter().fold(CCPLattice::top(), |val, id| {
            CCPLattice::meet(&val, &inputs[id.idx()])
        }),
        // If node has only one output, if doesn't directly handle crossover of
        // reachability and constant propagation. Projection handles that.
        Node::If { control, cond: _ } => inputs[control.idx()].clone(),
        Node::Match { control, sum: _ } => inputs[control.idx()].clone(),
        Node::Fork {
            control,
            factors: _,
        } => inputs[control.idx()].clone(),
        Node::Join { control } => inputs[control.idx()].clone(),
        // Phi nodes must look at the reachability of the inputs to its
        // corresponding region node to determine the constant value being
        // output.
        Node::Phi { control, data } => {
            // Get the control predecessors of the corresponding region.
            let region_preds = if let Node::Region { preds } = &editor.func().nodes[control.idx()] {
                preds
            } else {
                panic!("A phi's control input must be a region node.")
            };
            zip(region_preds.iter(), data.iter()).fold(
                CCPLattice {
                    reachability: inputs[control.idx()].reachability.clone(),
                    constant: ConstantLattice::top(),
                },
                |val, (control_id, data_id)| {
                    // If a control input to the region node is reachable, then
                    // and only then do we meet with the data input's constant
                    // lattice value.
                    if inputs[control_id.idx()].is_reachable() {
                        CCPLattice::meet(&val, &inputs[data_id.idx()])
                    } else {
                        val
                    }
                },
            )
        }
        // TODO: This should produce a constant zero if the dynamic constant for
        // for the corresponding fork is one.
        Node::ThreadID {
            control,
            dimension: _,
        } => inputs[control.idx()].clone(),
        Node::Reduce {
            control,
            init,
            reduct,
        } => {
            let reachability = inputs[control.idx()].reachability.clone();
            if reachability == ReachabilityLattice::Reachable {
                let mut constant = inputs[init.idx()].constant.clone();
                if inputs[reduct.idx()].is_reachable() {
                    constant = ConstantLattice::meet(&constant, &inputs[reduct.idx()].constant);
                }
                CCPLattice {
                    reachability,
                    constant,
                }
            } else {
                CCPLattice {
                    reachability,
                    constant: ConstantLattice::top(),
                }
            }
        }
        Node::Return { control, data: _ } => inputs[control.idx()].clone(),
        Node::Parameter { index: _ } => CCPLattice::bottom(),
        // A constant node is the "source" of concrete constant lattice values.
        Node::Constant { id } => CCPLattice {
            reachability: ReachabilityLattice::bottom(),
            constant: ConstantLattice::Constant(editor.get_constant(*id).clone()),
        },
        Node::DynamicConstant { id } => match *editor.get_dynamic_constant(*id) {
            DynamicConstant::Constant(value) => CCPLattice {
                reachability: ReachabilityLattice::bottom(),
                constant: ConstantLattice::Constant(Constant::UnsignedInteger64(value as u64)),
            },
            _ => CCPLattice::bottom(),
        },
        // Interpret unary op on constant.
        Node::Unary { input, op } => {
            let CCPLattice {
                ref reachability,
                ref constant,
            } = inputs[input.idx()];

            let new_constant = if let ConstantLattice::Constant(cons) = constant {
                match (op, cons) {
                    (UnaryOperator::Not, Constant::Boolean(val)) => Some(ConstantLattice::Constant(Constant::Boolean(!val))),
                    (UnaryOperator::Not, Constant::Integer8(val)) => Some(ConstantLattice::Constant(Constant::Integer8(!val))),
                    (UnaryOperator::Not, Constant::Integer16(val)) => Some(ConstantLattice::Constant(Constant::Integer16(!val))),
                    (UnaryOperator::Not, Constant::Integer32(val)) => Some(ConstantLattice::Constant(Constant::Integer32(!val))),
                    (UnaryOperator::Not, Constant::Integer64(val)) => Some(ConstantLattice::Constant(Constant::Integer64(!val))),
                    (UnaryOperator::Not, Constant::UnsignedInteger8(val)) => Some(ConstantLattice::Constant(Constant::UnsignedInteger8(!val))),
                    (UnaryOperator::Not, Constant::UnsignedInteger16(val)) => Some(ConstantLattice::Constant(Constant::UnsignedInteger16(!val))),
                    (UnaryOperator::Not, Constant::UnsignedInteger32(val)) => Some(ConstantLattice::Constant(Constant::UnsignedInteger32(!val))),
                    (UnaryOperator::Not, Constant::UnsignedInteger64(val)) => Some(ConstantLattice::Constant(Constant::UnsignedInteger64(!val))),
                    (UnaryOperator::Neg, Constant::Integer8(val)) => val.checked_neg().map(|v| ConstantLattice::Constant(Constant::Integer8(v))),
                    (UnaryOperator::Neg, Constant::Integer16(val)) => val.checked_neg().map(|v| ConstantLattice::Constant(Constant::Integer16(v))),
                    (UnaryOperator::Neg, Constant::Integer32(val)) => val.checked_neg().map(|v| ConstantLattice::Constant(Constant::Integer32(v))),
                    (UnaryOperator::Neg, Constant::Integer64(val)) => val.checked_neg().map(|v| ConstantLattice::Constant(Constant::Integer64(v))),
                    (UnaryOperator::Neg, Constant::Float32(val)) => Some(ConstantLattice::Constant(Constant::Float32(-val))),
                    (UnaryOperator::Neg, Constant::Float64(val)) => Some(ConstantLattice::Constant(Constant::Float64(-val))),
                    (UnaryOperator::Cast(_), _) => Some(ConstantLattice::Bottom),
                    _ => panic!("Unsupported combination of unary operation and constant value. Did typechecking succeed?")
                }.unwrap_or(ConstantLattice::bottom())
            } else {
                constant.clone()
            };

            CCPLattice {
                reachability: reachability.clone(),
                constant: new_constant,
            }
        }
        // Interpret binary op on constants.
        Node::Binary { left, right, op } => {
            let CCPLattice {
                reachability: ref left_reachability,
                constant: ref left_constant,
            } = inputs[left.idx()];
            let CCPLattice {
                reachability: ref right_reachability,
                constant: ref right_constant,
            } = inputs[right.idx()];

            let new_constant = if let (
                ConstantLattice::Constant(left_cons),
                ConstantLattice::Constant(right_cons),
            ) = (left_constant, right_constant)
            {
                let new_cons = match (op, left_cons, right_cons) {
                    (BinaryOperator::Add, Constant::Integer8(left_val), Constant::Integer8(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::Integer8(v)),
                    (BinaryOperator::Add, Constant::Integer16(left_val), Constant::Integer16(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::Integer16(v)),
                    (BinaryOperator::Add, Constant::Integer32(left_val), Constant::Integer32(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::Integer32(v)),
                    (BinaryOperator::Add, Constant::Integer64(left_val), Constant::Integer64(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::Integer64(v)),
                    (BinaryOperator::Add, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::UnsignedInteger8(v)),
                    (BinaryOperator::Add, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::UnsignedInteger16(v)),
                    (BinaryOperator::Add, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::UnsignedInteger32(v)),
                    (BinaryOperator::Add, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => left_val.checked_add(*right_val).map(|v| Constant::UnsignedInteger64(v)),
                    (BinaryOperator::Add, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Float32(*left_val + *right_val)),
                    (BinaryOperator::Add, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Float64(*left_val + *right_val)),
                    (BinaryOperator::Sub, Constant::Integer8(left_val), Constant::Integer8(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::Integer8(v)),
                    (BinaryOperator::Sub, Constant::Integer16(left_val), Constant::Integer16(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::Integer16(v)),
                    (BinaryOperator::Sub, Constant::Integer32(left_val), Constant::Integer32(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::Integer32(v)),
                    (BinaryOperator::Sub, Constant::Integer64(left_val), Constant::Integer64(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::Integer64(v)),
                    (BinaryOperator::Sub, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::UnsignedInteger8(v)),
                    (BinaryOperator::Sub, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::UnsignedInteger16(v)),
                    (BinaryOperator::Sub, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::UnsignedInteger32(v)),
                    (BinaryOperator::Sub, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => left_val.checked_sub(*right_val).map(|v| Constant::UnsignedInteger64(v)),
                    (BinaryOperator::Sub, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Float32(*left_val - *right_val)),
                    (BinaryOperator::Sub, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Float64(*left_val - *right_val)),
                    (BinaryOperator::Mul, Constant::Integer8(left_val), Constant::Integer8(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::Integer8(v)),
                    (BinaryOperator::Mul, Constant::Integer16(left_val), Constant::Integer16(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::Integer16(v)),
                    (BinaryOperator::Mul, Constant::Integer32(left_val), Constant::Integer32(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::Integer32(v)),
                    (BinaryOperator::Mul, Constant::Integer64(left_val), Constant::Integer64(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::Integer64(v)),
                    (BinaryOperator::Mul, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::UnsignedInteger8(v)),
                    (BinaryOperator::Mul, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::UnsignedInteger16(v)),
                    (BinaryOperator::Mul, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::UnsignedInteger32(v)),
                    (BinaryOperator::Mul, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => left_val.checked_mul(*right_val).map(|v| Constant::UnsignedInteger64(v)),
                    (BinaryOperator::Mul, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Float32(*left_val * *right_val)),
                    (BinaryOperator::Mul, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Float64(*left_val * *right_val)),
                    (BinaryOperator::Div, Constant::Integer8(left_val), Constant::Integer8(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::Integer8(v)),
                    (BinaryOperator::Div, Constant::Integer16(left_val), Constant::Integer16(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::Integer16(v)),
                    (BinaryOperator::Div, Constant::Integer32(left_val), Constant::Integer32(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::Integer32(v)),
                    (BinaryOperator::Div, Constant::Integer64(left_val), Constant::Integer64(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::Integer64(v)),
                    (BinaryOperator::Div, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::UnsignedInteger8(v)),
                    (BinaryOperator::Div, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::UnsignedInteger16(v)),
                    (BinaryOperator::Div, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::UnsignedInteger32(v)),
                    (BinaryOperator::Div, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => left_val.checked_div(*right_val).map(|v| Constant::UnsignedInteger64(v)),
                    (BinaryOperator::Div, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Float32(*left_val / *right_val)),
                    (BinaryOperator::Div, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Float64(*left_val / *right_val)),
                    (BinaryOperator::Rem, Constant::Integer8(left_val), Constant::Integer8(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::Integer8(v)),
                    (BinaryOperator::Rem, Constant::Integer16(left_val), Constant::Integer16(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::Integer16(v)),
                    (BinaryOperator::Rem, Constant::Integer32(left_val), Constant::Integer32(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::Integer32(v)),
                    (BinaryOperator::Rem, Constant::Integer64(left_val), Constant::Integer64(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::Integer64(v)),
                    (BinaryOperator::Rem, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::UnsignedInteger8(v)),
                    (BinaryOperator::Rem, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::UnsignedInteger16(v)),
                    (BinaryOperator::Rem, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::UnsignedInteger32(v)),
                    (BinaryOperator::Rem, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => left_val.checked_rem(*right_val).map(|v| Constant::UnsignedInteger64(v)),
                    (BinaryOperator::Rem, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Float32(*left_val % *right_val)),
                    (BinaryOperator::Rem, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Float64(*left_val % *right_val)),
                    (BinaryOperator::LT, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::Boolean(left_val < right_val)),
                    (BinaryOperator::LT, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Boolean(*left_val < *right_val)),
                    (BinaryOperator::LT, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Boolean(*left_val < *right_val)),
                    (BinaryOperator::LTE, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::Boolean(left_val <= right_val)),
                    (BinaryOperator::LTE, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Boolean(*left_val <= *right_val)),
                    (BinaryOperator::LTE, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Boolean(*left_val <= *right_val)),
                    (BinaryOperator::GT, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::Boolean(left_val > right_val)),
                    (BinaryOperator::GT, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Boolean(*left_val > *right_val)),
                    (BinaryOperator::GT, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Boolean(*left_val > *right_val)),
                    (BinaryOperator::GTE, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::Boolean(left_val >= right_val)),
                    (BinaryOperator::GTE, Constant::Float32(left_val), Constant::Float32(right_val)) => Some(Constant::Boolean(*left_val >= *right_val)),
                    (BinaryOperator::GTE, Constant::Float64(left_val), Constant::Float64(right_val)) => Some(Constant::Boolean(*left_val >= *right_val)),
                    // EQ and NE can be implemented more easily, since we don't
                    // need to unpack the constants.
                    (BinaryOperator::EQ, left_val, right_val) => Some(Constant::Boolean(left_val == right_val)),
                    (BinaryOperator::NE, left_val, right_val) => Some(Constant::Boolean(left_val != right_val)),
                    (BinaryOperator::Or, Constant::Boolean(left_val), Constant::Boolean(right_val)) => Some(Constant::Boolean(*left_val || *right_val)),
                    (BinaryOperator::Or, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Integer8(left_val | right_val)),
                    (BinaryOperator::Or, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Integer16(left_val | right_val)),
                    (BinaryOperator::Or, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Integer32(left_val | right_val)),
                    (BinaryOperator::Or, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Integer64(left_val | right_val)),
                    (BinaryOperator::Or, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::UnsignedInteger8(left_val | right_val)),
                    (BinaryOperator::Or, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::UnsignedInteger16(left_val | right_val)),
                    (BinaryOperator::Or, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::UnsignedInteger32(left_val | right_val)),
                    (BinaryOperator::Or, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::UnsignedInteger64(left_val | right_val)),
                    (BinaryOperator::And, Constant::Boolean(left_val), Constant::Boolean(right_val)) => Some(Constant::Boolean(*left_val && *right_val)),
                    (BinaryOperator::And, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Integer8(left_val & right_val)),
                    (BinaryOperator::And, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Integer16(left_val & right_val)),
                    (BinaryOperator::And, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Integer32(left_val & right_val)),
                    (BinaryOperator::And, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Integer64(left_val & right_val)),
                    (BinaryOperator::And, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::UnsignedInteger8(left_val & right_val)),
                    (BinaryOperator::And, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::UnsignedInteger16(left_val & right_val)),
                    (BinaryOperator::And, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::UnsignedInteger32(left_val & right_val)),
                    (BinaryOperator::And, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::UnsignedInteger64(left_val & right_val)),
                    (BinaryOperator::Xor, Constant::Boolean(left_val), Constant::Boolean(right_val)) => Some(Constant::Boolean(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Integer8(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Integer16(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Integer32(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Integer64(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::UnsignedInteger8(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::UnsignedInteger16(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::UnsignedInteger32(left_val ^ right_val)),
                    (BinaryOperator::Xor, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::UnsignedInteger64(left_val ^ right_val)),
                    (BinaryOperator::LSh, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Integer8(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Integer16(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Integer32(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Integer64(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::UnsignedInteger8(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::UnsignedInteger16(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::UnsignedInteger32(left_val << right_val)),
                    (BinaryOperator::LSh, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::UnsignedInteger64(left_val << right_val)),
                    (BinaryOperator::RSh, Constant::Integer8(left_val), Constant::Integer8(right_val)) => Some(Constant::Integer8(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::Integer16(left_val), Constant::Integer16(right_val)) => Some(Constant::Integer16(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::Integer32(left_val), Constant::Integer32(right_val)) => Some(Constant::Integer32(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::Integer64(left_val), Constant::Integer64(right_val)) => Some(Constant::Integer64(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::UnsignedInteger8(left_val), Constant::UnsignedInteger8(right_val)) => Some(Constant::UnsignedInteger8(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::UnsignedInteger16(left_val), Constant::UnsignedInteger16(right_val)) => Some(Constant::UnsignedInteger16(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::UnsignedInteger32(left_val), Constant::UnsignedInteger32(right_val)) => Some(Constant::UnsignedInteger32(left_val >> right_val)),
                    (BinaryOperator::RSh, Constant::UnsignedInteger64(left_val), Constant::UnsignedInteger64(right_val)) => Some(Constant::UnsignedInteger64(left_val >> right_val)),
                    _ => panic!("Unsupported combination of binary operation and constant values. Did typechecking succeed?")
                };
                new_cons
                    .map(|c| ConstantLattice::Constant(c))
                    .unwrap_or(ConstantLattice::bottom())
            } else if (left_constant.is_top() && !right_constant.is_bottom())
                || (!left_constant.is_bottom() && right_constant.is_top())
            {
                ConstantLattice::top()
            } else {
                ConstantLattice::meet(left_constant, right_constant)
            };

            CCPLattice {
                reachability: ReachabilityLattice::join(left_reachability, right_reachability),
                constant: new_constant,
            }
        }
        Node::Ternary {
            first,
            second,
            third,
            op,
        } => {
            let CCPLattice {
                reachability: ref first_reachability,
                constant: ref first_constant,
            } = inputs[first.idx()];
            let CCPLattice {
                reachability: ref second_reachability,
                constant: ref second_constant,
            } = inputs[second.idx()];
            let CCPLattice {
                reachability: ref third_reachability,
                constant: ref third_constant,
            } = inputs[third.idx()];

            let new_constant = if let (
                ConstantLattice::Constant(first_cons),
                ConstantLattice::Constant(second_cons),
                ConstantLattice::Constant(third_cons),
            ) = (first_constant, second_constant, third_constant)
            {
                let new_cons = match(op, first_cons, second_cons, third_cons) {
                    (TernaryOperator::Select, Constant::Boolean(first_val), second_val, third_val) => if *first_val {second_val.clone()} else {third_val.clone()},
                    _ => panic!("Unsupported combination of ternary operation and constant values. Did typechecking succeed?")
                };
                ConstantLattice::Constant(new_cons)
            } else if (first_constant.is_top()
                && !second_constant.is_bottom()
                && !third_constant.is_bottom())
                || (!first_constant.is_bottom()
                    && second_constant.is_top()
                    && !first_constant.is_bottom())
                || (!first_constant.is_bottom()
                    && !second_constant.is_bottom()
                    && third_constant.is_top())
            {
                ConstantLattice::top()
            } else {
                ConstantLattice::meet(second_constant, third_constant)
            };

            CCPLattice {
                reachability: ReachabilityLattice::join(
                    first_reachability,
                    &ReachabilityLattice::join(second_reachability, third_reachability),
                ),
                constant: new_constant,
            }
        }
        // Call nodes are uninterpretable.
        Node::Call {
            control: _,
            function: _,
            dynamic_constants: _,
            args,
        } => CCPLattice {
            reachability: args.iter().fold(ReachabilityLattice::bottom(), |val, id| {
                ReachabilityLattice::join(&val, &inputs[id.idx()].reachability)
            }),
            constant: ConstantLattice::bottom(),
        },
        // Data projections are uninterpretable.
        Node::DataProjection { data, selection: _ } => CCPLattice {
            reachability: inputs[data.idx()].reachability.clone(),
            constant: ConstantLattice::bottom(),
        },
        Node::IntrinsicCall { intrinsic, args } => {
            let mut new_reachability = ReachabilityLattice::bottom();
            let mut new_constant = ConstantLattice::top();
            let mut constants = vec![];
            let mut all_constants = true;

            for arg in args.iter() {
                let CCPLattice {
                    ref reachability,
                    ref constant,
                } = inputs[arg.idx()];

                new_reachability = ReachabilityLattice::join(&new_reachability, reachability);
                new_constant = ConstantLattice::meet(&new_constant, constant);

                if let ConstantLattice::Constant(constant) = constant {
                    constants.push(constant);
                } else {
                    all_constants = false;
                }
            }

            if all_constants {
                new_constant = match intrinsic {
                    Intrinsic::Abs => {
                        if let Constant::Integer8(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Integer8(i.abs()))
                        } else if let Constant::Integer16(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Integer16(i.abs()))
                        } else if let Constant::Integer32(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Integer32(i.abs()))
                        } else if let Constant::Integer64(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Integer64(i.abs()))
                        } else if let Constant::UnsignedInteger8(i) = constants[0] {
                            ConstantLattice::Constant(Constant::UnsignedInteger8(*i))
                        } else if let Constant::UnsignedInteger16(i) = constants[0] {
                            ConstantLattice::Constant(Constant::UnsignedInteger16(*i))
                        } else if let Constant::UnsignedInteger32(i) = constants[0] {
                            ConstantLattice::Constant(Constant::UnsignedInteger32(*i))
                        } else if let Constant::UnsignedInteger64(i) = constants[0] {
                            ConstantLattice::Constant(Constant::UnsignedInteger64(*i))
                        } else if let Constant::Float32(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Float32(
                                ordered_float::OrderedFloat(i.abs()),
                            ))
                        } else if let Constant::Float64(i) = constants[0] {
                            ConstantLattice::Constant(Constant::Float64(
                                ordered_float::OrderedFloat(i.abs()),
                            ))
                        } else {
                            panic!("Unsupported combination of intrinsic abs and constant value. Did typechecking succeed?")
                        }
                    }
                    Intrinsic::ACos => unary_float_intrinsic!(intrinsic, constants, acos),
                    Intrinsic::ACosh => unary_float_intrinsic!(intrinsic, constants, acosh),
                    Intrinsic::ASin => unary_float_intrinsic!(intrinsic, constants, asin),
                    Intrinsic::ASinh => unary_float_intrinsic!(intrinsic, constants, asinh),
                    Intrinsic::ATan => unary_float_intrinsic!(intrinsic, constants, atan),
                    Intrinsic::ATan2 => binary_float_intrinsic!(intrinsic, constants, atan2),
                    Intrinsic::ATanh => unary_float_intrinsic!(intrinsic, constants, atanh),
                    Intrinsic::Cbrt => unary_float_intrinsic!(intrinsic, constants, cbrt),
                    Intrinsic::Ceil => unary_float_intrinsic!(intrinsic, constants, ceil),
                    Intrinsic::Cos => unary_float_intrinsic!(intrinsic, constants, cos),
                    Intrinsic::Cosh => unary_float_intrinsic!(intrinsic, constants, cosh),
                    Intrinsic::Exp => unary_float_intrinsic!(intrinsic, constants, exp),
                    Intrinsic::Exp2 => unary_float_intrinsic!(intrinsic, constants, exp2),
                    Intrinsic::ExpM1 => unary_float_intrinsic!(intrinsic, constants, exp_m1),
                    Intrinsic::Floor => unary_float_intrinsic!(intrinsic, constants, floor),
                    Intrinsic::Ln => unary_float_intrinsic!(intrinsic, constants, ln),
                    Intrinsic::Ln1P => unary_float_intrinsic!(intrinsic, constants, ln_1p),
                    Intrinsic::Log => binary_float_intrinsic!(intrinsic, constants, log),
                    Intrinsic::Log10 => unary_float_intrinsic!(intrinsic, constants, log10),
                    Intrinsic::Log2 => unary_float_intrinsic!(intrinsic, constants, log2),
                    Intrinsic::Max => {
                        if let (Constant::Integer8(i), Constant::Integer8(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer8(max(*i, *j)))
                        } else if let (Constant::Integer16(i), Constant::Integer16(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer16(max(*i, *j)))
                        } else if let (Constant::Integer32(i), Constant::Integer32(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer32(max(*i, *j)))
                        } else if let (Constant::Integer64(i), Constant::Integer64(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer64(max(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger8(i),
                            Constant::UnsignedInteger8(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger8(max(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger16(i),
                            Constant::UnsignedInteger16(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger16(max(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger32(i),
                            Constant::UnsignedInteger32(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger32(max(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger64(i),
                            Constant::UnsignedInteger64(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger64(max(*i, *j)))
                        } else if let (Constant::Float32(i), Constant::Float32(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Float32(*i.max(j)))
                        } else if let (Constant::Float64(i), Constant::Float64(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Float64(*i.max(j)))
                        } else {
                            panic!("Unsupported combination of intrinsic abs and constant value. Did typechecking succeed?")
                        }
                    }
                    Intrinsic::Min => {
                        if let (Constant::Integer8(i), Constant::Integer8(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer8(min(*i, *j)))
                        } else if let (Constant::Integer16(i), Constant::Integer16(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer16(min(*i, *j)))
                        } else if let (Constant::Integer32(i), Constant::Integer32(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer32(min(*i, *j)))
                        } else if let (Constant::Integer64(i), Constant::Integer64(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Integer64(min(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger8(i),
                            Constant::UnsignedInteger8(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger8(min(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger16(i),
                            Constant::UnsignedInteger16(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger16(min(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger32(i),
                            Constant::UnsignedInteger32(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger32(min(*i, *j)))
                        } else if let (
                            Constant::UnsignedInteger64(i),
                            Constant::UnsignedInteger64(j),
                        ) = (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::UnsignedInteger64(min(*i, *j)))
                        } else if let (Constant::Float32(i), Constant::Float32(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Float32(*i.min(j)))
                        } else if let (Constant::Float64(i), Constant::Float64(j)) =
                            (constants[0], constants[1])
                        {
                            ConstantLattice::Constant(Constant::Float64(*i.min(j)))
                        } else {
                            panic!("Unsupported combination of intrinsic abs and constant value. Did typechecking succeed?")
                        }
                    }
                    Intrinsic::Pow => {
                        if let Constant::UnsignedInteger32(p) = constants[1] {
                            if let Constant::Integer8(i) = constants[0] {
                                ConstantLattice::Constant(Constant::Integer8(i.pow(*p)))
                            } else if let Constant::Integer16(i) = constants[0] {
                                ConstantLattice::Constant(Constant::Integer16(i.pow(*p)))
                            } else if let Constant::Integer32(i) = constants[0] {
                                ConstantLattice::Constant(Constant::Integer32(i.pow(*p)))
                            } else if let Constant::Integer64(i) = constants[0] {
                                ConstantLattice::Constant(Constant::Integer64(i.pow(*p)))
                            } else if let Constant::UnsignedInteger8(i) = constants[0] {
                                ConstantLattice::Constant(Constant::UnsignedInteger8(i.pow(*p)))
                            } else if let Constant::UnsignedInteger16(i) = constants[0] {
                                ConstantLattice::Constant(Constant::UnsignedInteger16(i.pow(*p)))
                            } else if let Constant::UnsignedInteger32(i) = constants[0] {
                                ConstantLattice::Constant(Constant::UnsignedInteger32(i.pow(*p)))
                            } else if let Constant::UnsignedInteger64(i) = constants[0] {
                                ConstantLattice::Constant(Constant::UnsignedInteger64(i.pow(*p)))
                            } else {
                                panic!("Unsupported combination of intrinsic pow and constant values. Did typechecking succeed?")
                            }
                        } else {
                            panic!("Unsupported combination of intrinsic pow and constant values. Did typechecking succeed?")
                        }
                    }
                    Intrinsic::Powf => binary_float_intrinsic!(intrinsic, constants, powf),
                    Intrinsic::Powi => {
                        if let Constant::Integer32(p) = constants[1] {
                            if let Constant::Float32(v) = constants[0] {
                                ConstantLattice::Constant(Constant::Float32(
                                    ordered_float::OrderedFloat(v.powi(*p)),
                                ))
                            } else if let Constant::Float64(v) = constants[0] {
                                ConstantLattice::Constant(Constant::Float64(
                                    ordered_float::OrderedFloat(v.powi(*p)),
                                ))
                            } else {
                                panic!("Unsupported combination of intrinsic powi and constant value. Did typechecking succeed?")
                            }
                        } else {
                            panic!("Unsupported combination of intrinsic powi and constant values. Did typechecking succeed?")
                        }
                    }
                    Intrinsic::Round => unary_float_intrinsic!(intrinsic, constants, round),
                    Intrinsic::Sin => unary_float_intrinsic!(intrinsic, constants, sin),
                    Intrinsic::Sinh => unary_float_intrinsic!(intrinsic, constants, sinh),
                    Intrinsic::Sqrt => unary_float_intrinsic!(intrinsic, constants, sqrt),
                    Intrinsic::Tan => unary_float_intrinsic!(intrinsic, constants, tan),
                    Intrinsic::Tanh => unary_float_intrinsic!(intrinsic, constants, tanh),
                };
            }

            CCPLattice {
                reachability: new_reachability,
                constant: new_constant,
            }
        }
        Node::LibraryCall {
            library_function: _,
            args,
            ty: _,
            device: _,
        } => CCPLattice {
            reachability: args.iter().fold(ReachabilityLattice::bottom(), |val, id| {
                ReachabilityLattice::join(&val, &inputs[id.idx()].reachability)
            }),
            constant: ConstantLattice::bottom(),
        },
        Node::Read { collect, indices } => {
            let mut reachability = inputs[collect.idx()].reachability.clone();
            for index in indices.iter() {
                if let Index::Position(positions) = index {
                    for position in positions.iter() {
                        reachability = ReachabilityLattice::join(
                            &reachability,
                            &inputs[position.idx()].reachability,
                        );
                    }
                }
            }
            CCPLattice {
                reachability,
                constant: ConstantLattice::bottom(),
            }
        }
        // Control projection handles reachability when following an if or match.
        Node::ControlProjection { control, selection } => match &editor.func().nodes[control.idx()]
        {
            Node::If { control: _, cond } => {
                let cond_constant = &inputs[cond.idx()].constant;
                let if_reachability = &inputs[control.idx()].reachability;
                let if_constant = &inputs[control.idx()].constant;

                let new_reachability = if cond_constant.is_top() {
                    ReachabilityLattice::top()
                } else if let ConstantLattice::Constant(cons) = cond_constant {
                    if let Constant::Boolean(val) = cons {
                        if *val && *selection == 0 {
                            // If condition is true and this is the false
                            // branch, then unreachable.
                            ReachabilityLattice::top()
                        } else if !val && *selection == 1 {
                            // If condition is true and this is the true branch,
                            // then unreachable.
                            ReachabilityLattice::top()
                        } else {
                            if_reachability.clone()
                        }
                    } else {
                        panic!("Attempted to interpret Read node, where corresponding if node has a non-boolean constant input. Did typechecking succeed?")
                    }
                } else {
                    if_reachability.clone()
                };

                CCPLattice {
                    reachability: new_reachability,
                    constant: if_constant.clone(),
                }
            }
            Node::Match { control: _, sum } => {
                let sum_constant = &inputs[sum.idx()].constant;
                let if_reachability = &inputs[control.idx()].reachability;
                let if_constant = &inputs[control.idx()].constant;

                let new_reachability = if sum_constant.is_top() {
                    ReachabilityLattice::top()
                } else if let ConstantLattice::Constant(cons) = sum_constant {
                    if let Constant::Summation(_, variant, _) = cons {
                        if *variant as usize != *selection {
                            // If match variant is not the same as this branch,
                            // then unreachable.
                            ReachabilityLattice::top()
                        } else {
                            if_reachability.clone()
                        }
                    } else {
                        panic!("Attempted to interpret projection node, where corresponding match node has a non-summation constant input. Did typechecking succeed?")
                    }
                } else {
                    if_reachability.clone()
                };

                CCPLattice {
                    reachability: new_reachability,
                    constant: if_constant.clone(),
                }
            }
            _ => panic!("Projection predecessor can only be an if or match node."),
        },
        // Write is uninterpreted for now.
        Node::Write {
            collect,
            data,
            indices,
        } => {
            let mut reachability = ReachabilityLattice::join(
                &inputs[collect.idx()].reachability,
                &inputs[data.idx()].reachability,
            );
            for index in indices.iter() {
                if let Index::Position(positions) = index {
                    for position in positions.iter() {
                        reachability = ReachabilityLattice::join(
                            &reachability,
                            &inputs[position.idx()].reachability,
                        );
                    }
                }
            }
            CCPLattice {
                reachability,
                constant: ConstantLattice::bottom(),
            }
        }
        Node::Undef { ty: _ } => CCPLattice {
            reachability: ReachabilityLattice::bottom(),
            constant: ConstantLattice::top(),
        },
    }
}