Restrict usages of type variables in non-generalized contexts

Type variables can only be used on functions (and in number literals as
a carve-out for now). In all other cases, a type variable takes on a
single, concrete type based on later usages. This check emits errors
when this is violated.

The implementation is to check the rank of a variable after it could be
generalized. If the variable is not generalized but annotated as a type
variable, emit an error.
This commit is contained in:
Ayaz Hafiz 2025-01-02 14:26:37 -06:00
parent f5961cbb22
commit a0461679dd
13 changed files with 230 additions and 114 deletions

View file

@ -15,15 +15,12 @@ use bumpalo::Bump;
use roc_can::abilities::{AbilitiesStore, MemberSpecializationInfo};
use roc_can::constraint::Constraint::{self, *};
use roc_can::constraint::{
Cycle, FxCallConstraint, FxSuffixConstraint, FxSuffixKind, LetConstraint, OpportunisticResolve,
TryTargetConstraint,
Cycle, FxCallConstraint, FxSuffixConstraint, FxSuffixKind, Generalizable, LetConstraint,
OpportunisticResolve, TryTargetConstraint,
};
use roc_can::expected::{Expected, PExpected};
use roc_can::module::ModuleParams;
use roc_collections::{VecMap, VecSet};
use roc_debug_flags::dbg_do;
#[cfg(debug_assertions)]
use roc_debug_flags::ROC_VERIFY_RIGID_LET_GENERALIZED;
use roc_error_macros::internal_error;
use roc_module::ident::IdentSuffix;
use roc_module::symbol::{ModuleId, Symbol};
@ -32,8 +29,8 @@ use roc_region::all::{Loc, Region};
use roc_solve_problem::TypeError;
use roc_solve_schema::UnificationMode;
use roc_types::subs::{
self, Content, FlatType, GetSubsSlice, Mark, OptVariable, Rank, Subs, TagExt, UlsOfVar,
Variable,
self, Content, ErrorTypeContext, FlatType, GetSubsSlice, Mark, OptVariable, Rank, Subs, TagExt,
UlsOfVar, Variable,
};
use roc_types::types::{Category, Polarity, Reason, RecordField, Type, TypeExtension, Types, Uls};
use roc_unify::unify::{
@ -356,29 +353,13 @@ fn solve(
generalize(env, young_mark, visit_mark, rank.next());
debug_assert!(env.pools.get(rank.next()).is_empty(), "variables left over in let-binding scope, but they should all be in a lower scope or generalized now");
// check that things went well
dbg_do!(ROC_VERIFY_RIGID_LET_GENERALIZED, {
let rigid_vars = &env.constraints[let_con.rigid_vars];
// NOTE the `subs.redundant` check does not come from elm.
// It's unclear whether this is a bug with our implementation
// (something is redundant that shouldn't be)
// or that it just never came up in elm.
let mut it = rigid_vars
.iter()
.filter(|loc_var| {
let var = loc_var.value;
!env.subs.redundant(var) && env.subs.get_rank(var) != Rank::GENERALIZED
})
.peekable();
if it.peek().is_some() {
let failing: Vec<_> = it.collect();
println!("Rigids {:?}", &rigid_vars);
println!("Failing {failing:?}");
debug_assert!(false);
}
});
let named_variables = &env.constraints[let_con.rigid_vars];
check_named_variables_are_generalized(
env,
problems,
named_variables,
let_con.generalizable,
);
let mut new_scope = scope.clone();
for (symbol, loc_var) in local_def_vars.iter() {
@ -1636,6 +1617,30 @@ fn solve(
state
}
fn check_named_variables_are_generalized(
env: &mut InferenceEnv<'_>,
problems: &mut Vec<TypeError>,
named_variables: &[Loc<Variable>],
generalizable: Generalizable,
) {
for loc_var in named_variables {
let is_generalized = env.subs.get_rank(loc_var.value) == Rank::GENERALIZED;
if !is_generalized {
// TODO: should be OF_PATTERN if on the LHS of a function, otherwise OF_VALUE.
let polarity = Polarity::OF_VALUE;
let ctx = ErrorTypeContext::NON_GENERALIZED_AS_INFERRED;
let error_type = env
.subs
.var_to_error_type_contextual(loc_var.value, ctx, polarity);
problems.push(TypeError::TypeIsNotGeneralized(
loc_var.region,
error_type,
generalizable,
));
}
}
}
fn solve_suffix_fx(
env: &mut InferenceEnv<'_>,
problems: &mut Vec<TypeError>,