Provide warning for defs that are used only in (mutual) recursion

This patch provides errors for defs that are used only in
possibly-mutual recursion, and are not reachable outside of their
recursive closures. For example:

```
test_report!(
    mutual_recursion_not_reached_nested,
    indoc!(
        r#"
        app "test" provides [main] to "./platform"

        main =
            f = \{} -> if Bool.true then "" else g {}
            g = \{} -> if Bool.true then "" else f {}
            ""
        "#
    ),
@r###"
── DEFINITIONs ONLY USED IN RECURSION ──────────────────── /code/proj/Main.roc ─

These 2 definitions are only used in mutual recursion with themselves:

4│>      f = \{} -> if Bool.true then "" else g {}
5│>      g = \{} -> if Bool.true then "" else f {}

If you don't intend to use or export any of them, they should all be
removed!
"###
);
```
This commit is contained in:
Ayaz Hafiz 2022-12-01 18:26:01 -06:00
parent 1beb00f490
commit 0a807dc43e
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
8 changed files with 262 additions and 15 deletions

View file

@ -301,7 +301,7 @@ fn sort_type_defs_before_introduction(
matrix
.strongly_connected_components_all()
.groups()
.flat_map(|group| group.iter_ones())
.flat_map(|(group, _)| group.iter_ones())
.map(|index| symbols[index])
.collect()
}
@ -1546,10 +1546,12 @@ impl DefOrdering {
#[inline(always)]
pub(crate) fn sort_can_defs_new(
env: &mut Env<'_>,
scope: &mut Scope,
var_store: &mut VarStore,
defs: CanDefs,
mut output: Output,
exposed_symbols: &VecSet<Symbol>,
) -> (Declarations, Output) {
let CanDefs {
defs,
@ -1610,7 +1612,7 @@ pub(crate) fn sort_can_defs_new(
sccs.reorder(&mut defs);
for group in sccs.groups().rev() {
for (group, is_initial) in sccs.groups().rev() {
match group.count_ones() {
1 => {
// a group with a single Def, nice and simple
@ -1635,6 +1637,10 @@ pub(crate) fn sort_can_defs_new(
}
};
if is_initial && !exposed_symbols.contains(&symbol) {
env.problem(Problem::DefsOnlyUsedInRecursion(1, def.region()));
}
match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_recursive_def(
@ -1719,6 +1725,9 @@ pub(crate) fn sort_can_defs_new(
let cycle_mark = IllegalCycleMark::new(var_store);
declarations.push_recursive_group(group_length as u16, cycle_mark);
let mut group_is_initial = is_initial;
let mut whole_region = None;
// then push the definitions of this group
for def in group_defs {
let (symbol, specializes) = match def.loc_pattern.value {
@ -1733,6 +1742,12 @@ pub(crate) fn sort_can_defs_new(
}
};
group_is_initial = group_is_initial && !exposed_symbols.contains(&symbol);
whole_region = match whole_region {
None => Some(def.region()),
Some(r) => Some(Region::span_across(&r, &def.region())),
};
match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_recursive_def(
@ -1754,6 +1769,13 @@ pub(crate) fn sort_can_defs_new(
}
}
}
if group_is_initial {
env.problem(Problem::DefsOnlyUsedInRecursion(
group_length,
whole_region.unwrap(),
));
}
}
}
}
@ -1802,7 +1824,7 @@ pub(crate) fn sort_can_defs(
let mut declarations = Vec::with_capacity(defs.len());
for group in sccs.groups() {
for (group, is_initial) in sccs.groups() {
if group.count_ones() == 1 {
// a group with a single Def, nice and simple
let index = group.iter_ones().next().unwrap();
@ -1816,6 +1838,16 @@ pub(crate) fn sort_can_defs(
let declaration = if def_ordering.references.get_row_col(index, index) {
debug_assert!(!is_specialization, "Self-recursive specializations can only be determined during solving - but it was determined for {:?} now, that's a bug!", def);
if is_initial
&& !def
.pattern_vars
.keys()
.any(|sym| output.references.has_value_lookup(*sym))
{
// This defs is only used in recursion with itself.
env.problem(Problem::DefsOnlyUsedInRecursion(1, def.region()));
}
// this function calls itself, and must be typechecked as a recursive def
Declaration::DeclareRec(vec![mark_def_recursive(def)], IllegalCycleMark::empty())
} else {
@ -1861,11 +1893,26 @@ pub(crate) fn sort_can_defs(
Declaration::InvalidCycle(entries)
} else {
let rec_defs = group
let rec_defs: Vec<Def> = group
.iter_ones()
.map(|index| mark_def_recursive(take_def!(index)))
.collect();
if is_initial
&& !rec_defs.iter().any(|def| {
def.pattern_vars
.keys()
.any(|sym| output.references.has_value_lookup(*sym))
})
{
// These defs are only used in mutual recursion with themselves.
let region = Region::span_across(
&rec_defs.first().unwrap().region(),
&rec_defs.last().unwrap().region(),
);
env.problem(Problem::DefsOnlyUsedInRecursion(rec_defs.len(), region));
}
Declaration::DeclareRec(rec_defs, IllegalCycleMark::new(var_store))
};
@ -2311,10 +2358,15 @@ pub fn can_defs_with_return<'a>(
output
.introduced_variables
.union(&defs_output.introduced_variables);
// Sort the defs with the output of the return expression - we'll use this to catch unused defs
// due only to recursion.
let (declarations, mut output) = sort_can_defs(env, var_store, unsorted, output);
output.references.union_mut(&defs_output.references);
// Now that we've collected all the references, check to see if any of the new idents
// we defined went unused by the return expression. If any were unused, report it.
// we defined went unused by the return expression or any other def.
for (symbol, region) in symbols_introduced {
if !output.references.has_type_or_value_lookup(symbol)
&& !scope.abilities_store.is_specialization_name(symbol)
@ -2323,8 +2375,6 @@ pub fn can_defs_with_return<'a>(
}
}
let (declarations, output) = sort_can_defs(env, var_store, unsorted, output);
let mut loc_expr: Loc<Expr> = ret_expr;
for declaration in declarations.into_iter().rev() {
@ -2735,12 +2785,12 @@ fn correct_mutual_recursive_type_alias<'a>(
// this is needed.
let scratchpad_capacity = sccs
.groups()
.map(|r| r.count_ones())
.map(|(r, _)| r.count_ones())
.max()
.unwrap_or_default();
let mut scratchpad = Vec::with_capacity(scratchpad_capacity);
for cycle in sccs.groups() {
for (cycle, _is_initial) in sccs.groups() {
debug_assert!(cycle.count_ones() > 0);
// We need to instantiate the alias with any symbols in the currrent module it

View file

@ -1411,6 +1411,13 @@ fn canonicalize_closure_body<'a>(
}
}
// // Discount the def name from the referenced arguments as well. This catches recursive
// // functions that only reference themselves.
// // Note that this will not catch mutually recursive functions whose reference closure is the
// // recursive closure. Doing so requires us to know what defs are in a cycle, which is only
// // calculated after enumeration of references.
// output.references.remove_value_lookup(&symbol);
// store the references of this function in the Env. This information is used
// when we canonicalize a surrounding def (if it exists)
env.closures.insert(symbol, output.references.clone());

View file

@ -376,6 +376,9 @@ pub fn canonicalize_module_defs<'a>(
// See if any of the new idents we defined went unused.
// If any were unused and also not exposed, report it.
//
// We'll catch symbols that are only referenced due to (mutual) recursion later,
// when sorting the defs.
for (symbol, region) in symbols_introduced {
if !output.references.has_type_or_value_lookup(symbol)
&& !exposed_symbols.contains(&symbol)
@ -427,8 +430,14 @@ pub fn canonicalize_module_defs<'a>(
..Default::default()
};
let (mut declarations, mut output) =
crate::def::sort_can_defs_new(&mut scope, var_store, defs, new_output);
let (mut declarations, mut output) = crate::def::sort_can_defs_new(
&mut env,
&mut scope,
var_store,
defs,
new_output,
exposed_symbols,
);
debug_assert!(
output.pending_derives.is_empty(),