diff --git a/juno_scheduler/src/compile.rs b/juno_scheduler/src/compile.rs
index 6b40001c2b3324176913b1e843934d422ed2e711..e96a818adb71a028cc4dc308085416519d7753cd 100644
--- a/juno_scheduler/src/compile.rs
+++ b/juno_scheduler/src/compile.rs
@@ -73,6 +73,9 @@ struct MacroInfo {
 
 enum Appliable {
     Pass(ir::Pass),
+    // DeleteUncalled requires special handling because it changes FunctionIDs, so it is not
+    // treated like a pass
+    DeleteUncalled,
     Schedule(Schedule),
     Device(Device),
 }
@@ -81,7 +84,7 @@ impl Appliable {
     fn num_args(&self) -> usize {
         match self {
             Appliable::Pass(pass) => pass.num_args(),
-            // Schedules and devices do not arguments (at the moment)
+            // Delete uncalled, Schedules, and devices do not take arguments
             _ => 0,
         }
     }
@@ -97,7 +100,7 @@ impl FromStr for Appliable {
             "ccp" => Ok(Appliable::Pass(ir::Pass::CCP)),
             "crc" | "collapse-read-chains" => Ok(Appliable::Pass(ir::Pass::CRC)),
             "dce" => Ok(Appliable::Pass(ir::Pass::DCE)),
-            "delete-uncalled" => Ok(Appliable::Pass(ir::Pass::DeleteUncalled)),
+            "delete-uncalled" => Ok(Appliable::DeleteUncalled),
             "float-collections" | "collections" => Ok(Appliable::Pass(ir::Pass::FloatCollections)),
             "fork-guard-elim" => Ok(Appliable::Pass(ir::Pass::ForkGuardElim)),
             "fork-split" => Ok(Appliable::Pass(ir::Pass::ForkSplit)),
@@ -323,6 +326,11 @@ fn compile_expr(
                     args: arg_vals,
                     on: selection,
                 })),
+                Appliable::DeleteUncalled => {
+                    Ok(ExprResult::Expr(ir::ScheduleExp::DeleteUncalled {
+                        on: selection,
+                    }))
+                }
                 Appliable::Schedule(sched) => Ok(ExprResult::Stmt(ir::ScheduleStmt::AddSchedule {
                     sched,
                     on: selection,
diff --git a/juno_scheduler/src/default.rs b/juno_scheduler/src/default.rs
index 3f4af107c45d87e6a89b198483b25abae6156a78..507b40338fd6495ef32fc20c1aa2d2be817b7797 100644
--- a/juno_scheduler/src/default.rs
+++ b/juno_scheduler/src/default.rs
@@ -2,6 +2,14 @@ use crate::ir::*;
 
 #[macro_export]
 macro_rules! pass {
+    (DeleteUncalled) => {
+        ScheduleStmt::Let {
+            var: String::from("_"),
+            exp: ScheduleExp::DeleteUncalled {
+                on: Selector::Everything(),
+            },
+        }
+    };
     ($p:ident) => {
         ScheduleStmt::Let {
             var: String::from("_"),
@@ -40,7 +48,7 @@ pub fn default_schedule() -> ScheduleStmt {
         SLF,
         DCE,
         Inline,
-        /*DeleteUncalled,*/
+        DeleteUncalled,
         InterproceduralSROA,
         SROA,
         PhiElim,
diff --git a/juno_scheduler/src/ir.rs b/juno_scheduler/src/ir.rs
index 840f25a6e9dc986ab064adecbeba822ca47016d8..1c594f1594dbdcf4e31bc05ca518644df6fd3605 100644
--- a/juno_scheduler/src/ir.rs
+++ b/juno_scheduler/src/ir.rs
@@ -7,7 +7,6 @@ pub enum Pass {
     CCP,
     CRC,
     DCE,
-    DeleteUncalled,
     FloatCollections,
     ForkChunk,
     ForkCoalesce,
@@ -76,6 +75,9 @@ pub enum ScheduleExp {
         args: Vec<ScheduleExp>,
         on: Selector,
     },
+    DeleteUncalled {
+        on: Selector,
+    },
     Record {
         fields: Vec<(String, ScheduleExp)>,
     },
diff --git a/juno_scheduler/src/pm.rs b/juno_scheduler/src/pm.rs
index f59834eddad670cceeb4954812fff5926f39f862..326087212b0133ac9106c785f1b4a627f0043a55 100644
--- a/juno_scheduler/src/pm.rs
+++ b/juno_scheduler/src/pm.rs
@@ -543,6 +543,15 @@ impl PassManager {
         }
     }
 
+    pub fn fix_deleted_functions(&mut self, id_mapping: &[Option<usize>]) {
+        let mut idx = 0;
+
+        self.functions.retain(|_| {
+            idx += 1;
+            id_mapping[idx - 1].is_some()
+        });
+    }
+
     fn clear_analyses(&mut self) {
         self.def_uses = None;
         self.reverse_postorders = None;
@@ -848,12 +857,12 @@ pub fn schedule_codegen(
     schedule: ScheduleStmt,
     mut stringtab: StringTable,
     mut env: Env<usize, Value>,
-    functions: JunoFunctions,
+    mut functions: JunoFunctions,
     output_dir: String,
     module_name: String,
 ) -> Result<(), SchedulerError> {
     let mut pm = PassManager::new(module);
-    let _ = schedule_interpret(&mut pm, &schedule, &mut stringtab, &mut env, &functions)?;
+    let _ = schedule_interpret(&mut pm, &schedule, &mut stringtab, &mut env, &mut functions)?;
     pm.codegen(output_dir, module_name)
 }
 
@@ -862,10 +871,10 @@ pub fn schedule_module(
     schedule: ScheduleStmt,
     mut stringtab: StringTable,
     mut env: Env<usize, Value>,
-    functions: JunoFunctions,
+    mut functions: JunoFunctions,
 ) -> Result<Module, SchedulerError> {
     let mut pm = PassManager::new(module);
-    let _ = schedule_interpret(&mut pm, &schedule, &mut stringtab, &mut env, &functions)?;
+    let _ = schedule_interpret(&mut pm, &schedule, &mut stringtab, &mut env, &mut functions)?;
     Ok(pm.get_module())
 }
 
@@ -877,7 +886,7 @@ fn schedule_interpret(
     schedule: &ScheduleStmt,
     stringtab: &mut StringTable,
     env: &mut Env<usize, Value>,
-    functions: &JunoFunctions,
+    functions: &mut JunoFunctions,
 ) -> Result<bool, SchedulerError> {
     match schedule {
         ScheduleStmt::Fixpoint { body, limit } => {
@@ -977,7 +986,7 @@ fn interp_expr(
     expr: &ScheduleExp,
     stringtab: &mut StringTable,
     env: &mut Env<usize, Value>,
-    functions: &JunoFunctions,
+    functions: &mut JunoFunctions,
 ) -> Result<(Value, bool), SchedulerError> {
     match expr {
         ScheduleExp::Variable { var } => {
@@ -1070,6 +1079,78 @@ fn interp_expr(
             changed |= modified;
             Ok((res, changed))
         }
+        ScheduleExp::DeleteUncalled { on } => {
+            let Selector::Everything() = on else {
+                return Err(SchedulerError::PassError {
+                    pass: "DeleteUncalled".to_string(),
+                    error: "must be applied to the entire module".to_string(),
+                });
+            };
+
+            pm.make_callgraph();
+            pm.make_def_uses();
+            let callgraph = pm.callgraph.take().unwrap();
+            let def_uses = pm.def_uses.take().unwrap();
+
+            let mut editors: Vec<_> = pm
+                .functions
+                .iter_mut()
+                .enumerate()
+                .zip(def_uses.iter())
+                .map(|((idx, func), def_use)| {
+                    FunctionEditor::new(
+                        func,
+                        FunctionID::new(idx),
+                        &pm.constants,
+                        &pm.dynamic_constants,
+                        &pm.types,
+                        &pm.labels,
+                        def_use,
+                    )
+                })
+                .collect();
+
+            let new_idx = delete_uncalled(&mut editors, &callgraph);
+            let changed = new_idx.iter().any(|i| i.is_none());
+
+            pm.fix_deleted_functions(&new_idx);
+            pm.delete_gravestones();
+            pm.clear_analyses();
+            assert!(pm.functions.len() > 0, "PANIC: There are no entry functions in the Hercules module being compiled. Please mark at least one function as an entry!");
+
+            // Update all FunctionIDs contained in both the environment and
+            // "functions" data structure to point to the new values. If there
+            // is no new value (the function refered to no longer exists) then
+            // we drop the value from the environment/functions list
+
+            // Updating Juno functions may result in all instances of a function being deleted
+            // which can cause renumbering of the Juno functions as well, so we do that first
+            let mut new_juno_idx = vec![];
+            let mut new_juno_funcs = vec![];
+            for funcs in std::mem::take(&mut functions.func_ids).into_iter() {
+                let new_funcs = funcs
+                    .into_iter()
+                    .filter_map(|f| new_idx[f.idx()].map(|i| FunctionID::new(i)))
+                    .collect::<Vec<_>>();
+                if !new_funcs.is_empty() {
+                    new_juno_idx.push(Some(new_juno_funcs.len()));
+                    new_juno_funcs.push(new_funcs);
+                } else {
+                    new_juno_idx.push(None);
+                }
+            }
+            functions.func_ids = new_juno_funcs;
+
+            // Now, we update both the FunctionIDs and JunoFunctionIDs in the environment
+            env.filter_map(|val| update_value(val, &new_idx, &new_juno_idx));
+
+            Ok((
+                Value::Record {
+                    fields: HashMap::new(),
+                },
+                changed,
+            ))
+        }
         ScheduleExp::Record { fields } => {
             let mut result = HashMap::new();
             let mut changed = false;
@@ -1108,6 +1189,80 @@ fn interp_expr(
     }
 }
 
+fn update_value(
+    val: Value,
+    func_idx: &[Option<usize>],
+    juno_func_idx: &[Option<usize>],
+) -> Option<Value> {
+    match val {
+        // For a label (which may refer to labels in multiple functions) we update our labels to
+        // point to the new functions (and eliminate any which were to now gone functions). If
+        // there are no labels left, remove this value since it refers to nothing
+        Value::Label { labels } => {
+            let new_labels = labels
+                .into_iter()
+                .filter_map(|LabelInfo { func, label }| {
+                    func_idx[func.idx()].clone().map(|i| LabelInfo {
+                        func: FunctionID::new(i),
+                        label,
+                    })
+                })
+                .collect::<Vec<_>>();
+            if new_labels.is_empty() {
+                None
+            } else {
+                Some(Value::Label { labels: new_labels })
+            }
+        }
+        // Similar approach for selections, update each value and if nothing remains just drop the
+        // whole value
+        Value::Selection { selection } => {
+            let new_selection = selection
+                .into_iter()
+                .filter_map(|v| update_value(v, func_idx, juno_func_idx))
+                .collect::<Vec<_>>();
+            if new_selection.is_empty() {
+                None
+            } else {
+                Some(Value::Selection {
+                    selection: new_selection,
+                })
+            }
+        }
+        // And similarly for records (this one might seem a little odd, but it means that if an
+        // optimization returned data for multiple functions we'll delete the fields that refered
+        // to those functions but keep around fields that still hold useful values)
+        Value::Record { fields } => {
+            let new_fields = fields
+                .into_iter()
+                .filter_map(|(f, v)| update_value(v, func_idx, juno_func_idx).map(|v| (f, v)))
+                .collect::<HashMap<_, _>>();
+            if new_fields.is_empty() {
+                None
+            } else {
+                Some(Value::Record { fields: new_fields })
+            }
+        }
+        Value::JunoFunction { func } => {
+            juno_func_idx[func.idx]
+                .clone()
+                .map(|i| Value::JunoFunction {
+                    func: JunoFunctionID::new(i),
+                })
+        }
+        Value::HerculesFunction { func } => {
+            func_idx[func.idx()]
+                .clone()
+                .map(|i| Value::HerculesFunction {
+                    func: FunctionID::new(i),
+                })
+        }
+        Value::Everything {} => Some(Value::Everything {}),
+        Value::Integer { val } => Some(Value::Integer { val }),
+        Value::Boolean { val } => Some(Value::Boolean { val }),
+    }
+}
+
 fn add_schedule(pm: &mut PassManager, sched: Schedule, label_ids: Vec<LabelInfo>) {
     for LabelInfo { func, label } in label_ids {
         let nodes = pm.functions[func.idx()]
@@ -1489,9 +1644,6 @@ fn run_pass(
             pm.delete_gravestones();
             pm.clear_analyses();
         }
-        Pass::DeleteUncalled => {
-            todo!("Delete Uncalled changes FunctionIDs, a bunch of bookkeeping is needed for the pass manager to address this")
-        }
         Pass::FloatCollections => {
             assert!(args.is_empty());
             pm.make_typing();
diff --git a/juno_utils/src/env.rs b/juno_utils/src/env.rs
index cfa84b7875be3f5154cf4051d136ed234d75cd39..93395ce8c37fa03177914fbcd5882e354886f24e 100644
--- a/juno_utils/src/env.rs
+++ b/juno_utils/src/env.rs
@@ -74,4 +74,51 @@ impl<K: Eq + Hash + Copy, V> Env<K, V> {
         self.count += 1;
         n
     }
+
+    pub fn filter_map<F>(&mut self, mut f: F)
+    where
+        F: FnMut(V) -> Option<V>,
+    {
+        // To update the environment we have to associate values in the table with the scopes so
+        // that if we delete a value from the table we delete it's note in the scope as well
+        let num_scopes = self.scope.len();
+
+        // To do this, we first construct a map from keys to a (sorted) list of the scopes that
+        // have bindings of that key.
+        let mut scopes: HashMap<K, Vec<usize>> = HashMap::new();
+        for (scope, keys) in std::mem::take(&mut self.scope).into_iter().enumerate() {
+            for k in keys {
+                scopes.entry(k).or_insert(vec![]).push(scope);
+            }
+        }
+
+        // Now, we can process the actual table and the scopes table in parallel since they have
+        // matching structure
+        let mut new_table = HashMap::new();
+        let mut new_scopes: HashMap<K, Vec<usize>> = HashMap::new();
+
+        for (k, vs) in std::mem::take(&mut self.table) {
+            let scope_list = scopes.remove(&k).unwrap();
+            assert!(scope_list.len() == vs.len());
+
+            let (new_vals, new_scope_list) = vs
+                .into_iter()
+                .zip(scope_list.into_iter())
+                .filter_map(|(v, s)| f(v).map(|v| (v, s)))
+                .unzip();
+
+            new_table.insert(k, new_vals);
+            new_scopes.insert(k, new_scope_list);
+        }
+
+        // Finally we reconstruct the actual environment
+        self.table = new_table;
+        let mut scope: Vec<HashSet<K>> = vec![HashSet::new(); num_scopes];
+        for (k, scopes) in new_scopes {
+            for s in scopes {
+                scope[s].insert(k);
+            }
+        }
+        self.scope = scope;
+    }
 }