mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-03 16:44:33 +00:00
Merge remote-tracking branch 'origin/trunk' into builtins-in-roc-delayed-alias
This commit is contained in:
commit
4e1197165b
181 changed files with 9495 additions and 2273 deletions
|
@ -1,41 +1,61 @@
|
|||
use crate::solve;
|
||||
use crate::solve::{self, Aliases};
|
||||
use roc_can::constraint::{Constraint as ConstraintSoa, Constraints};
|
||||
use roc_can::module::RigidVariables;
|
||||
use roc_collections::all::MutMap;
|
||||
use roc_module::ident::Lowercase;
|
||||
use roc_module::symbol::Symbol;
|
||||
use roc_types::solved_types::{Solved, SolvedType};
|
||||
use roc_types::subs::{Subs, VarStore, Variable};
|
||||
use roc_types::subs::{StorageSubs, Subs, Variable};
|
||||
use roc_types::types::Alias;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SolvedModule {
|
||||
pub solved_types: MutMap<Symbol, SolvedType>,
|
||||
pub aliases: MutMap<Symbol, Alias>,
|
||||
pub exposed_symbols: Vec<Symbol>,
|
||||
pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>,
|
||||
pub problems: Vec<solve::TypeError>,
|
||||
|
||||
/// all aliases and their definitions. this has to include non-exposed aliases
|
||||
/// because exposed aliases can depend on non-exposed ones)
|
||||
pub aliases: MutMap<Symbol, Alias>,
|
||||
|
||||
/// Used when the goal phase is TypeChecking, and
|
||||
/// to create the types for HostExposed. This
|
||||
/// has some overlap with the StorageSubs fields,
|
||||
/// so maybe we can get rid of this at some point
|
||||
pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>,
|
||||
|
||||
/// Used when importing this module into another module
|
||||
pub stored_vars_by_symbol: Vec<(Symbol, Variable)>,
|
||||
pub storage_subs: StorageSubs,
|
||||
}
|
||||
|
||||
pub fn run_solve(
|
||||
constraints: &Constraints,
|
||||
constraint: ConstraintSoa,
|
||||
rigid_variables: MutMap<Variable, Lowercase>,
|
||||
var_store: VarStore,
|
||||
rigid_variables: RigidVariables,
|
||||
mut subs: Subs,
|
||||
mut aliases: Aliases,
|
||||
) -> (Solved<Subs>, solve::Env, Vec<solve::TypeError>) {
|
||||
let env = solve::Env::default();
|
||||
|
||||
let mut subs = Subs::new_from_varstore(var_store);
|
||||
|
||||
for (var, name) in rigid_variables {
|
||||
for (var, name) in rigid_variables.named {
|
||||
subs.rigid_var(var, name);
|
||||
}
|
||||
|
||||
for var in rigid_variables.wildcards {
|
||||
subs.rigid_var(var, "*".into());
|
||||
}
|
||||
|
||||
// Now that the module is parsed, canonicalized, and constrained,
|
||||
// we need to type check it.
|
||||
let mut problems = Vec::new();
|
||||
|
||||
// Run the solver to populate Subs.
|
||||
let (solved_subs, solved_env) = solve::run(constraints, &env, &mut problems, subs, &constraint);
|
||||
let (solved_subs, solved_env) = solve::run(
|
||||
constraints,
|
||||
&env,
|
||||
&mut problems,
|
||||
subs,
|
||||
&mut aliases,
|
||||
&constraint,
|
||||
);
|
||||
|
||||
(solved_subs, solved_env, problems)
|
||||
}
|
||||
|
@ -59,3 +79,19 @@ pub fn make_solved_types(
|
|||
|
||||
solved_types
|
||||
}
|
||||
|
||||
pub fn exposed_types_storage_subs(
|
||||
solved_subs: &mut Solved<Subs>,
|
||||
exposed_vars_by_symbol: &[(Symbol, Variable)],
|
||||
) -> (StorageSubs, Vec<(Symbol, Variable)>) {
|
||||
let subs = solved_subs.inner_mut();
|
||||
let mut storage_subs = StorageSubs::new(Subs::new());
|
||||
let mut stored_vars_by_symbol = Vec::with_capacity(exposed_vars_by_symbol.len());
|
||||
|
||||
for (symbol, var) in exposed_vars_by_symbol.iter() {
|
||||
let new_var = storage_subs.import_variable_from(subs, *var).variable;
|
||||
stored_vars_by_symbol.push((*symbol, new_var));
|
||||
}
|
||||
|
||||
(storage_subs, stored_vars_by_symbol)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ use roc_types::subs::{
|
|||
};
|
||||
use roc_types::types::Type::{self, *};
|
||||
use roc_types::types::{
|
||||
gather_fields_unsorted_iter, AliasKind, Category, ErrorType, PatternCategory,
|
||||
gather_fields_unsorted_iter, AliasCommon, AliasKind, Category, ErrorType, PatternCategory,
|
||||
TypeExtension,
|
||||
};
|
||||
use roc_unify::unify::{unify, Mode, Unified::*};
|
||||
|
||||
|
@ -76,6 +77,343 @@ pub enum TypeError {
|
|||
UnexposedLookup(Symbol),
|
||||
}
|
||||
|
||||
use roc_types::types::Alias;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DelayedAliasVariables {
|
||||
start: u32,
|
||||
type_variables_len: u8,
|
||||
lambda_set_variables_len: u8,
|
||||
recursion_variables_len: u8,
|
||||
}
|
||||
|
||||
impl DelayedAliasVariables {
|
||||
fn recursion_variables(self, variables: &mut [Variable]) -> &mut [Variable] {
|
||||
let start = self.start as usize
|
||||
+ (self.type_variables_len + self.lambda_set_variables_len) as usize;
|
||||
let length = self.recursion_variables_len as usize;
|
||||
|
||||
&mut variables[start..][..length]
|
||||
}
|
||||
|
||||
fn lambda_set_variables(self, variables: &mut [Variable]) -> &mut [Variable] {
|
||||
let start = self.start as usize + self.type_variables_len as usize;
|
||||
let length = self.lambda_set_variables_len as usize;
|
||||
|
||||
&mut variables[start..][..length]
|
||||
}
|
||||
|
||||
fn type_variables(self, variables: &mut [Variable]) -> &mut [Variable] {
|
||||
let start = self.start as usize;
|
||||
let length = self.type_variables_len as usize;
|
||||
|
||||
&mut variables[start..][..length]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Aliases {
|
||||
aliases: Vec<(Symbol, Type, DelayedAliasVariables)>,
|
||||
variables: Vec<Variable>,
|
||||
}
|
||||
|
||||
impl Aliases {
|
||||
pub fn insert(&mut self, symbol: Symbol, alias: Alias) {
|
||||
// debug_assert!(self.get(&symbol).is_none());
|
||||
|
||||
let alias_variables =
|
||||
{
|
||||
let start = self.variables.len() as _;
|
||||
|
||||
self.variables
|
||||
.extend(alias.type_variables.iter().map(|x| x.value.1));
|
||||
|
||||
self.variables.extend(alias.lambda_set_variables.iter().map(
|
||||
|x| match x.as_inner() {
|
||||
Type::Variable(v) => *v,
|
||||
_ => unreachable!("lambda set type is not a variable"),
|
||||
},
|
||||
));
|
||||
|
||||
let recursion_variables_len = alias.recursion_variables.len() as _;
|
||||
self.variables.extend(alias.recursion_variables);
|
||||
|
||||
DelayedAliasVariables {
|
||||
start,
|
||||
type_variables_len: alias.type_variables.len() as _,
|
||||
lambda_set_variables_len: alias.lambda_set_variables.len() as _,
|
||||
recursion_variables_len,
|
||||
}
|
||||
};
|
||||
|
||||
self.aliases.push((symbol, alias.typ, alias_variables));
|
||||
}
|
||||
|
||||
fn instantiate_result_result(
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
alias_variables: AliasVariables,
|
||||
) -> Variable {
|
||||
let tag_names_slice = Subs::RESULT_TAG_NAMES;
|
||||
|
||||
let err_slice = SubsSlice::new(alias_variables.variables_start + 1, 1);
|
||||
let ok_slice = SubsSlice::new(alias_variables.variables_start, 1);
|
||||
|
||||
let variable_slices =
|
||||
SubsSlice::extend_new(&mut subs.variable_slices, [err_slice, ok_slice]);
|
||||
|
||||
let union_tags = UnionTags::from_slices(tag_names_slice, variable_slices);
|
||||
let ext_var = Variable::EMPTY_TAG_UNION;
|
||||
let flat_type = FlatType::TagUnion(union_tags, ext_var);
|
||||
let content = Content::Structure(flat_type);
|
||||
|
||||
register(subs, rank, pools, content)
|
||||
}
|
||||
|
||||
/// Instantiate an alias of the form `Foo a : [ @Foo a ]`
|
||||
fn instantiate_num_at_alias(
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
tag_name_slice: SubsSlice<TagName>,
|
||||
range_slice: SubsSlice<Variable>,
|
||||
) -> Variable {
|
||||
let variable_slices = SubsSlice::extend_new(&mut subs.variable_slices, [range_slice]);
|
||||
|
||||
let union_tags = UnionTags::from_slices(tag_name_slice, variable_slices);
|
||||
let ext_var = Variable::EMPTY_TAG_UNION;
|
||||
let flat_type = FlatType::TagUnion(union_tags, ext_var);
|
||||
let content = Content::Structure(flat_type);
|
||||
|
||||
register(subs, rank, pools, content)
|
||||
}
|
||||
|
||||
fn instantiate_builtin_aliases(
|
||||
&mut self,
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
symbol: Symbol,
|
||||
alias_variables: AliasVariables,
|
||||
) -> Option<Variable> {
|
||||
match symbol {
|
||||
Symbol::RESULT_RESULT => {
|
||||
let var = Self::instantiate_result_result(subs, rank, pools, alias_variables);
|
||||
|
||||
Some(var)
|
||||
}
|
||||
Symbol::NUM_NUM => {
|
||||
let var = Self::instantiate_num_at_alias(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Subs::NUM_AT_NUM,
|
||||
SubsSlice::new(alias_variables.variables_start, 1),
|
||||
);
|
||||
|
||||
Some(var)
|
||||
}
|
||||
Symbol::NUM_FLOATINGPOINT => {
|
||||
let var = Self::instantiate_num_at_alias(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Subs::NUM_AT_FLOATINGPOINT,
|
||||
SubsSlice::new(alias_variables.variables_start, 1),
|
||||
);
|
||||
|
||||
Some(var)
|
||||
}
|
||||
Symbol::NUM_INTEGER => {
|
||||
let var = Self::instantiate_num_at_alias(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Subs::NUM_AT_INTEGER,
|
||||
SubsSlice::new(alias_variables.variables_start, 1),
|
||||
);
|
||||
|
||||
Some(var)
|
||||
}
|
||||
Symbol::NUM_INT => {
|
||||
// [ @Integer range ]
|
||||
let integer_content_var = Self::instantiate_builtin_aliases(
|
||||
self,
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Symbol::NUM_INTEGER,
|
||||
alias_variables,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Integer range (alias variable is the same as `Int range`)
|
||||
let integer_alias_variables = alias_variables;
|
||||
let integer_content = Content::Alias(
|
||||
Symbol::NUM_INTEGER,
|
||||
integer_alias_variables,
|
||||
integer_content_var,
|
||||
AliasKind::Structural,
|
||||
);
|
||||
let integer_alias_var = register(subs, rank, pools, integer_content);
|
||||
|
||||
// [ @Num (Integer range) ]
|
||||
let num_alias_variables =
|
||||
AliasVariables::insert_into_subs(subs, [integer_alias_var], []);
|
||||
let num_content_var = Self::instantiate_builtin_aliases(
|
||||
self,
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Symbol::NUM_NUM,
|
||||
num_alias_variables,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let num_content = Content::Alias(
|
||||
Symbol::NUM_NUM,
|
||||
num_alias_variables,
|
||||
num_content_var,
|
||||
AliasKind::Structural,
|
||||
);
|
||||
|
||||
Some(register(subs, rank, pools, num_content))
|
||||
}
|
||||
Symbol::NUM_FLOAT => {
|
||||
// [ @FloatingPoint range ]
|
||||
let fpoint_content_var = Self::instantiate_builtin_aliases(
|
||||
self,
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Symbol::NUM_FLOATINGPOINT,
|
||||
alias_variables,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// FloatingPoint range (alias variable is the same as `Float range`)
|
||||
let fpoint_alias_variables = alias_variables;
|
||||
let fpoint_content = Content::Alias(
|
||||
Symbol::NUM_FLOATINGPOINT,
|
||||
fpoint_alias_variables,
|
||||
fpoint_content_var,
|
||||
AliasKind::Structural,
|
||||
);
|
||||
let fpoint_alias_var = register(subs, rank, pools, fpoint_content);
|
||||
|
||||
// [ @Num (FloatingPoint range) ]
|
||||
let num_alias_variables =
|
||||
AliasVariables::insert_into_subs(subs, [fpoint_alias_var], []);
|
||||
let num_content_var = Self::instantiate_builtin_aliases(
|
||||
self,
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
Symbol::NUM_NUM,
|
||||
num_alias_variables,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let num_content = Content::Alias(
|
||||
Symbol::NUM_NUM,
|
||||
num_alias_variables,
|
||||
num_content_var,
|
||||
AliasKind::Structural,
|
||||
);
|
||||
|
||||
Some(register(subs, rank, pools, num_content))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn instantiate(
|
||||
&mut self,
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
arena: &bumpalo::Bump,
|
||||
symbol: Symbol,
|
||||
alias_variables: AliasVariables,
|
||||
) -> Result<Variable, ()> {
|
||||
// hardcoded instantiations for builtin aliases
|
||||
if let Some(var) =
|
||||
Self::instantiate_builtin_aliases(self, subs, rank, pools, symbol, alias_variables)
|
||||
{
|
||||
return Ok(var);
|
||||
}
|
||||
|
||||
let (typ, delayed_variables) = match self.aliases.iter_mut().find(|(s, _, _)| *s == symbol)
|
||||
{
|
||||
None => return Err(()),
|
||||
Some((_, typ, delayed_variables)) => (typ, delayed_variables),
|
||||
};
|
||||
|
||||
let mut substitutions: MutMap<_, _> = Default::default();
|
||||
|
||||
for rec_var in delayed_variables
|
||||
.recursion_variables(&mut self.variables)
|
||||
.iter_mut()
|
||||
{
|
||||
let new_var = subs.fresh_unnamed_flex_var();
|
||||
substitutions.insert(*rec_var, new_var);
|
||||
*rec_var = new_var;
|
||||
}
|
||||
|
||||
let old_type_variables = delayed_variables.type_variables(&mut self.variables);
|
||||
let new_type_variables = &subs.variables[alias_variables.type_variables().indices()];
|
||||
|
||||
for (old, new) in old_type_variables.iter_mut().zip(new_type_variables) {
|
||||
// if constraint gen duplicated a type these variables could be the same
|
||||
// (happens very often in practice)
|
||||
if *old != *new {
|
||||
substitutions.insert(*old, *new);
|
||||
|
||||
*old = *new;
|
||||
}
|
||||
}
|
||||
|
||||
let old_lambda_set_variables = delayed_variables.lambda_set_variables(&mut self.variables);
|
||||
let new_lambda_set_variables =
|
||||
&subs.variables[alias_variables.lambda_set_variables().indices()];
|
||||
|
||||
for (old, new) in old_lambda_set_variables
|
||||
.iter_mut()
|
||||
.zip(new_lambda_set_variables)
|
||||
{
|
||||
if *old != *new {
|
||||
substitutions.insert(*old, *new);
|
||||
*old = *new;
|
||||
}
|
||||
}
|
||||
|
||||
if !substitutions.is_empty() {
|
||||
typ.substitute_variables(&substitutions);
|
||||
}
|
||||
|
||||
// assumption: an alias does not (transitively) syntactically contain itself
|
||||
// (if it did it would have to be a recursive tag union)
|
||||
let mut t = Type::EmptyRec;
|
||||
|
||||
std::mem::swap(typ, &mut t);
|
||||
|
||||
let alias_variable = type_to_variable(subs, rank, pools, arena, self, &t);
|
||||
|
||||
{
|
||||
match self.aliases.iter_mut().find(|(s, _, _)| *s == symbol) {
|
||||
None => unreachable!(),
|
||||
Some((_, typ, _)) => {
|
||||
// swap typ back
|
||||
std::mem::swap(typ, &mut t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(alias_variable)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Env {
|
||||
symbols: Vec<Symbol>,
|
||||
|
@ -173,9 +511,10 @@ pub fn run(
|
|||
env: &Env,
|
||||
problems: &mut Vec<TypeError>,
|
||||
mut subs: Subs,
|
||||
aliases: &mut Aliases,
|
||||
constraint: &Constraint,
|
||||
) -> (Solved<Subs>, Env) {
|
||||
let env = run_in_place(constraints, env, problems, &mut subs, constraint);
|
||||
let env = run_in_place(constraints, env, problems, &mut subs, aliases, constraint);
|
||||
|
||||
(Solved(subs), env)
|
||||
}
|
||||
|
@ -186,9 +525,11 @@ pub fn run_in_place(
|
|||
env: &Env,
|
||||
problems: &mut Vec<TypeError>,
|
||||
subs: &mut Subs,
|
||||
aliases: &mut Aliases,
|
||||
constraint: &Constraint,
|
||||
) -> Env {
|
||||
let mut pools = Pools::default();
|
||||
|
||||
let state = State {
|
||||
env: env.clone(),
|
||||
mark: Mark::NONE.next(),
|
||||
|
@ -205,7 +546,7 @@ pub fn run_in_place(
|
|||
rank,
|
||||
&mut pools,
|
||||
problems,
|
||||
&mut MutMap::default(),
|
||||
aliases,
|
||||
subs,
|
||||
constraint,
|
||||
);
|
||||
|
@ -225,6 +566,12 @@ enum Work<'a> {
|
|||
env: &'a Env,
|
||||
rank: Rank,
|
||||
let_con: &'a LetConstraint,
|
||||
|
||||
/// The variables used to store imported types in the Subs.
|
||||
/// The `Contents` are copied from the source module, but to
|
||||
/// mimic `type_to_var`, we must add these variables to `Pools`
|
||||
/// at the correct rank
|
||||
pool_variables: &'a [Variable],
|
||||
},
|
||||
/// The ret_con part of a let constraint that introduces rigid and/or flex variables
|
||||
///
|
||||
|
@ -234,6 +581,12 @@ enum Work<'a> {
|
|||
env: &'a Env,
|
||||
rank: Rank,
|
||||
let_con: &'a LetConstraint,
|
||||
|
||||
/// The variables used to store imported types in the Subs.
|
||||
/// The `Contents` are copied from the source module, but to
|
||||
/// mimic `type_to_var`, we must add these variables to `Pools`
|
||||
/// at the correct rank
|
||||
pool_variables: &'a [Variable],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -246,7 +599,7 @@ fn solve(
|
|||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
problems: &mut Vec<TypeError>,
|
||||
cached_aliases: &mut MutMap<Symbol, Variable>,
|
||||
aliases: &mut Aliases,
|
||||
subs: &mut Subs,
|
||||
constraint: &Constraint,
|
||||
) -> State {
|
||||
|
@ -277,7 +630,12 @@ fn solve(
|
|||
|
||||
continue;
|
||||
}
|
||||
Work::LetConNoVariables { env, rank, let_con } => {
|
||||
Work::LetConNoVariables {
|
||||
env,
|
||||
rank,
|
||||
let_con,
|
||||
pool_variables,
|
||||
} => {
|
||||
// NOTE be extremely careful with shadowing here
|
||||
let offset = let_con.defs_and_ret_constraint.index();
|
||||
let ret_constraint = &constraints.constraints[offset + 1];
|
||||
|
@ -287,11 +645,13 @@ fn solve(
|
|||
constraints,
|
||||
rank,
|
||||
pools,
|
||||
cached_aliases,
|
||||
aliases,
|
||||
subs,
|
||||
let_con.def_types,
|
||||
);
|
||||
|
||||
pools.get_mut(rank).extend(pool_variables);
|
||||
|
||||
let mut new_env = env.clone();
|
||||
for (symbol, loc_var) in local_def_vars.iter() {
|
||||
new_env.insert_symbol_var_if_vacant(*symbol, loc_var.value);
|
||||
|
@ -306,7 +666,12 @@ fn solve(
|
|||
|
||||
continue;
|
||||
}
|
||||
Work::LetConIntroducesVariables { env, rank, let_con } => {
|
||||
Work::LetConIntroducesVariables {
|
||||
env,
|
||||
rank,
|
||||
let_con,
|
||||
pool_variables,
|
||||
} => {
|
||||
// NOTE be extremely careful with shadowing here
|
||||
let offset = let_con.defs_and_ret_constraint.index();
|
||||
let ret_constraint = &constraints.constraints[offset + 1];
|
||||
|
@ -325,11 +690,13 @@ fn solve(
|
|||
constraints,
|
||||
next_rank,
|
||||
pools,
|
||||
cached_aliases,
|
||||
aliases,
|
||||
subs,
|
||||
let_con.def_types,
|
||||
);
|
||||
|
||||
pools.get_mut(next_rank).extend(pool_variables);
|
||||
|
||||
debug_assert_eq!(
|
||||
{
|
||||
let offenders = pools
|
||||
|
@ -414,18 +781,13 @@ fn solve(
|
|||
copy
|
||||
}
|
||||
Eq(type_index, expectation_index, category_index, region) => {
|
||||
let typ = &constraints.types[type_index.index()];
|
||||
let expectation = &constraints.expectations[expectation_index.index()];
|
||||
let category = &constraints.categories[category_index.index()];
|
||||
|
||||
let actual = type_to_var(subs, rank, pools, cached_aliases, typ);
|
||||
let expected = type_to_var(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
cached_aliases,
|
||||
expectation.get_type_ref(),
|
||||
);
|
||||
let actual =
|
||||
either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index);
|
||||
|
||||
let expectation = &constraints.expectations[expectation_index.index()];
|
||||
let expected = type_to_var(subs, rank, pools, aliases, expectation.get_type_ref());
|
||||
|
||||
match unify(subs, actual, expected, Mode::EQ) {
|
||||
Success(vars) => {
|
||||
|
@ -457,11 +819,16 @@ fn solve(
|
|||
}
|
||||
}
|
||||
Store(source_index, target, _filename, _linenr) => {
|
||||
let source = &constraints.types[source_index.index()];
|
||||
|
||||
// a special version of Eq that is used to store types in the AST.
|
||||
// IT DOES NOT REPORT ERRORS!
|
||||
let actual = type_to_var(subs, rank, pools, cached_aliases, source);
|
||||
let actual = either_type_index_to_var(
|
||||
constraints,
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
aliases,
|
||||
*source_index,
|
||||
);
|
||||
let target = *target;
|
||||
|
||||
match unify(subs, actual, target, Mode::EQ) {
|
||||
|
@ -513,13 +880,8 @@ fn solve(
|
|||
let actual = deep_copy_var_in(subs, rank, pools, var, arena);
|
||||
let expectation = &constraints.expectations[expectation_index.index()];
|
||||
|
||||
let expected = type_to_var(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
cached_aliases,
|
||||
expectation.get_type_ref(),
|
||||
);
|
||||
let expected =
|
||||
type_to_var(subs, rank, pools, aliases, expectation.get_type_ref());
|
||||
|
||||
match unify(subs, actual, expected, Mode::EQ) {
|
||||
Success(vars) => {
|
||||
|
@ -575,18 +937,13 @@ fn solve(
|
|||
}
|
||||
Pattern(type_index, expectation_index, category_index, region)
|
||||
| PatternPresence(type_index, expectation_index, category_index, region) => {
|
||||
let typ = &constraints.types[type_index.index()];
|
||||
let expectation = &constraints.pattern_expectations[expectation_index.index()];
|
||||
let category = &constraints.pattern_categories[category_index.index()];
|
||||
|
||||
let actual = type_to_var(subs, rank, pools, cached_aliases, typ);
|
||||
let expected = type_to_var(
|
||||
subs,
|
||||
rank,
|
||||
pools,
|
||||
cached_aliases,
|
||||
expectation.get_type_ref(),
|
||||
);
|
||||
let actual =
|
||||
either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index);
|
||||
|
||||
let expectation = &constraints.pattern_expectations[expectation_index.index()];
|
||||
let expected = type_to_var(subs, rank, pools, aliases, expectation.get_type_ref());
|
||||
|
||||
let mode = match constraint {
|
||||
PatternPresence(..) => Mode::PRESENT,
|
||||
|
@ -622,7 +979,7 @@ fn solve(
|
|||
}
|
||||
}
|
||||
}
|
||||
Let(index) => {
|
||||
Let(index, pool_slice) => {
|
||||
let let_con = &constraints.let_constraints[index.index()];
|
||||
|
||||
let offset = let_con.defs_and_ret_constraint.index();
|
||||
|
@ -632,7 +989,11 @@ fn solve(
|
|||
let flex_vars = &constraints.variables[let_con.flex_vars.indices()];
|
||||
let rigid_vars = &constraints.variables[let_con.rigid_vars.indices()];
|
||||
|
||||
let pool_variables = &constraints.variables[pool_slice.indices()];
|
||||
|
||||
if matches!(&ret_constraint, True) && let_con.rigid_vars.is_empty() {
|
||||
debug_assert!(pool_variables.is_empty());
|
||||
|
||||
introduce(subs, rank, pools, flex_vars);
|
||||
|
||||
// If the return expression is guaranteed to solve,
|
||||
|
@ -650,7 +1011,12 @@ fn solve(
|
|||
//
|
||||
// Note that the LetConSimple gets the current env and rank,
|
||||
// and not the env/rank from after solving the defs_constraint
|
||||
stack.push(Work::LetConNoVariables { env, rank, let_con });
|
||||
stack.push(Work::LetConNoVariables {
|
||||
env,
|
||||
rank,
|
||||
let_con,
|
||||
pool_variables,
|
||||
});
|
||||
stack.push(Work::Constraint {
|
||||
env,
|
||||
rank,
|
||||
|
@ -692,7 +1058,12 @@ fn solve(
|
|||
//
|
||||
// Note that the LetConSimple gets the current env and rank,
|
||||
// and not the env/rank from after solving the defs_constraint
|
||||
stack.push(Work::LetConIntroducesVariables { env, rank, let_con });
|
||||
stack.push(Work::LetConIntroducesVariables {
|
||||
env,
|
||||
rank,
|
||||
let_con,
|
||||
pool_variables,
|
||||
});
|
||||
stack.push(Work::Constraint {
|
||||
env,
|
||||
rank: next_rank,
|
||||
|
@ -703,9 +1074,9 @@ fn solve(
|
|||
}
|
||||
}
|
||||
IsOpenType(type_index) => {
|
||||
let typ = &constraints.types[type_index.index()];
|
||||
let actual =
|
||||
either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index);
|
||||
|
||||
let actual = type_to_var(subs, rank, pools, cached_aliases, typ);
|
||||
let mut new_desc = subs.get(actual);
|
||||
match new_desc.content {
|
||||
Content::Structure(FlatType::TagUnion(tags, _)) => {
|
||||
|
@ -741,12 +1112,12 @@ fn solve(
|
|||
let tys = &constraints.types[types.indices()];
|
||||
let pattern_category = &constraints.pattern_categories[pattern_category.index()];
|
||||
|
||||
let actual = type_to_var(subs, rank, pools, cached_aliases, typ);
|
||||
let actual = type_to_var(subs, rank, pools, aliases, typ);
|
||||
let tag_ty = Type::TagUnion(
|
||||
vec![(tag_name.clone(), tys.to_vec())],
|
||||
Box::new(Type::EmptyTagUnion),
|
||||
TypeExtension::Closed,
|
||||
);
|
||||
let includes = type_to_var(subs, rank, pools, cached_aliases, &tag_ty);
|
||||
let includes = type_to_var(subs, rank, pools, aliases, &tag_ty);
|
||||
|
||||
match unify(subs, actual, includes, Mode::PRESENT) {
|
||||
Success(vars) => {
|
||||
|
@ -818,7 +1189,7 @@ impl LocalDefVarsVec<(Symbol, Loc<Variable>)> {
|
|||
constraints: &Constraints,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
cached_aliases: &mut MutMap<Symbol, Variable>,
|
||||
aliases: &mut Aliases,
|
||||
subs: &mut Subs,
|
||||
def_types_slice: roc_can::constraint::DefTypes,
|
||||
) -> Self {
|
||||
|
@ -828,7 +1199,7 @@ impl LocalDefVarsVec<(Symbol, Loc<Variable>)> {
|
|||
let mut local_def_vars = Self::with_length(types_slice.len());
|
||||
|
||||
for ((symbol, region), typ) in loc_symbols_slice.iter().copied().zip(types_slice) {
|
||||
let var = type_to_var(subs, rank, pools, cached_aliases, typ);
|
||||
let var = type_to_var(subs, rank, pools, aliases, typ);
|
||||
|
||||
local_def_vars.push((symbol, Loc { value: var, region }));
|
||||
}
|
||||
|
@ -853,11 +1224,32 @@ fn put_scratchpad(scratchpad: bumpalo::Bump) {
|
|||
});
|
||||
}
|
||||
|
||||
fn either_type_index_to_var(
|
||||
constraints: &Constraints,
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
aliases: &mut Aliases,
|
||||
either_type_index: roc_collections::soa::EitherIndex<Type, Variable>,
|
||||
) -> Variable {
|
||||
match either_type_index.split() {
|
||||
Ok(type_index) => {
|
||||
let typ = &constraints.types[type_index.index()];
|
||||
|
||||
type_to_var(subs, rank, pools, aliases, typ)
|
||||
}
|
||||
Err(var_index) => {
|
||||
// we cheat, and store the variable directly in the index
|
||||
unsafe { Variable::from_index(var_index.index() as _) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn type_to_var(
|
||||
subs: &mut Subs,
|
||||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
_: &mut MutMap<Symbol, Variable>,
|
||||
aliases: &mut Aliases,
|
||||
typ: &Type,
|
||||
) -> Variable {
|
||||
if let Type::Variable(var) = typ {
|
||||
|
@ -866,7 +1258,7 @@ fn type_to_var(
|
|||
let mut arena = take_scratchpad();
|
||||
|
||||
// let var = type_to_variable(subs, rank, pools, &arena, typ);
|
||||
let var = type_to_variable(subs, rank, pools, &arena, typ);
|
||||
let var = type_to_variable(subs, rank, pools, &arena, aliases, typ);
|
||||
|
||||
arena.reset();
|
||||
put_scratchpad(arena);
|
||||
|
@ -897,6 +1289,20 @@ impl RegisterVariable {
|
|||
Variable(var) => Direct(*var),
|
||||
EmptyRec => Direct(Variable::EMPTY_RECORD),
|
||||
EmptyTagUnion => Direct(Variable::EMPTY_TAG_UNION),
|
||||
Type::DelayedAlias(AliasCommon { symbol, .. }) => {
|
||||
if let Some(reserved) = Variable::get_reserved(*symbol) {
|
||||
if rank.is_none() {
|
||||
// reserved variables are stored with rank NONE
|
||||
return Direct(reserved);
|
||||
} else {
|
||||
// for any other rank, we need to copy; it takes care of adjusting the rank
|
||||
let copied = deep_copy_var_in(subs, rank, pools, reserved, arena);
|
||||
return Direct(copied);
|
||||
}
|
||||
}
|
||||
|
||||
Deferred
|
||||
}
|
||||
Type::Alias { symbol, .. } => {
|
||||
if let Some(reserved) = Variable::get_reserved(*symbol) {
|
||||
if rank.is_none() {
|
||||
|
@ -945,6 +1351,7 @@ fn type_to_variable<'a>(
|
|||
rank: Rank,
|
||||
pools: &mut Pools,
|
||||
arena: &'a bumpalo::Bump,
|
||||
aliases: &mut Aliases,
|
||||
typ: &Type,
|
||||
) -> Variable {
|
||||
use bumpalo::collections::Vec;
|
||||
|
@ -1022,7 +1429,7 @@ fn type_to_variable<'a>(
|
|||
Record(fields, ext) => {
|
||||
// An empty fields is inefficient (but would be correct)
|
||||
// If hit, try to turn the value into an EmptyRecord in canonicalization
|
||||
debug_assert!(!fields.is_empty() || !ext.is_empty_record());
|
||||
debug_assert!(!fields.is_empty() || !ext.is_closed());
|
||||
|
||||
let mut field_vars = Vec::with_capacity_in(fields.len(), arena);
|
||||
|
||||
|
@ -1039,7 +1446,10 @@ fn type_to_variable<'a>(
|
|||
field_vars.push((field.clone(), field_var));
|
||||
}
|
||||
|
||||
let temp_ext_var = helper!(ext);
|
||||
let temp_ext_var = match ext {
|
||||
TypeExtension::Open(ext) => helper!(ext),
|
||||
TypeExtension::Closed => Variable::EMPTY_RECORD,
|
||||
};
|
||||
|
||||
let (it, new_ext_var) =
|
||||
gather_fields_unsorted_iter(subs, RecordFields::empty(), temp_ext_var)
|
||||
|
@ -1062,7 +1472,7 @@ fn type_to_variable<'a>(
|
|||
TagUnion(tags, ext) => {
|
||||
// An empty tags is inefficient (but would be correct)
|
||||
// If hit, try to turn the value into an EmptyTagUnion in canonicalization
|
||||
debug_assert!(!tags.is_empty() || !ext.is_empty_tag_union());
|
||||
debug_assert!(!tags.is_empty() || !ext.is_closed());
|
||||
|
||||
let (union_tags, ext) =
|
||||
type_to_union_tags(subs, rank, pools, arena, tags, ext, &mut stack);
|
||||
|
@ -1071,7 +1481,10 @@ fn type_to_variable<'a>(
|
|||
register_with_known_var(subs, destination, rank, pools, content)
|
||||
}
|
||||
FunctionOrTagUnion(tag_name, symbol, ext) => {
|
||||
let temp_ext_var = helper!(ext);
|
||||
let temp_ext_var = match ext {
|
||||
TypeExtension::Open(ext) => helper!(ext),
|
||||
TypeExtension::Closed => Variable::EMPTY_TAG_UNION,
|
||||
};
|
||||
|
||||
let (it, ext) = roc_types::types::gather_tags_unsorted_iter(
|
||||
subs,
|
||||
|
@ -1093,7 +1506,7 @@ fn type_to_variable<'a>(
|
|||
RecursiveTagUnion(rec_var, tags, ext) => {
|
||||
// An empty tags is inefficient (but would be correct)
|
||||
// If hit, try to turn the value into an EmptyTagUnion in canonicalization
|
||||
debug_assert!(!tags.is_empty() || !ext.is_empty_tag_union());
|
||||
debug_assert!(!tags.is_empty() || !ext.is_closed());
|
||||
|
||||
let (union_tags, ext) =
|
||||
type_to_union_tags(subs, rank, pools, arena, tags, ext, &mut stack);
|
||||
|
@ -1117,6 +1530,51 @@ fn type_to_variable<'a>(
|
|||
tag_union_var
|
||||
}
|
||||
|
||||
Type::DelayedAlias(AliasCommon {
|
||||
symbol,
|
||||
type_arguments,
|
||||
lambda_set_variables,
|
||||
}) => {
|
||||
let kind = AliasKind::Structural;
|
||||
|
||||
let alias_variables = {
|
||||
let length = type_arguments.len() + lambda_set_variables.len();
|
||||
let new_variables = VariableSubsSlice::reserve_into_subs(subs, length);
|
||||
|
||||
for (target_index, (_, arg_type)) in
|
||||
(new_variables.indices()).zip(type_arguments)
|
||||
{
|
||||
let copy_var = helper!(arg_type);
|
||||
subs.variables[target_index] = copy_var;
|
||||
}
|
||||
|
||||
let it = (new_variables.indices().skip(type_arguments.len()))
|
||||
.zip(lambda_set_variables);
|
||||
for (target_index, ls) in it {
|
||||
let copy_var = helper!(&ls.0);
|
||||
subs.variables[target_index] = copy_var;
|
||||
}
|
||||
|
||||
AliasVariables {
|
||||
variables_start: new_variables.start,
|
||||
type_variables_len: type_arguments.len() as _,
|
||||
all_variables_len: length as _,
|
||||
}
|
||||
};
|
||||
|
||||
let instantiated =
|
||||
aliases.instantiate(subs, rank, pools, arena, *symbol, alias_variables);
|
||||
|
||||
let alias_variable = match instantiated {
|
||||
Err(_) => unreachable!("Alias {:?} is not available", symbol),
|
||||
Ok(alias_variable) => alias_variable,
|
||||
};
|
||||
|
||||
let content = Content::Alias(*symbol, alias_variables, alias_variable, kind);
|
||||
|
||||
register_with_known_var(subs, destination, rank, pools, content)
|
||||
}
|
||||
|
||||
Type::Alias {
|
||||
symbol,
|
||||
type_arguments,
|
||||
|
@ -1187,7 +1645,8 @@ fn type_to_variable<'a>(
|
|||
};
|
||||
|
||||
// cannot use helper! here because this variable may be involved in unification below
|
||||
let alias_variable = type_to_variable(subs, rank, pools, arena, alias_type);
|
||||
let alias_variable =
|
||||
type_to_variable(subs, rank, pools, arena, aliases, alias_type);
|
||||
// TODO(opaques): I think host-exposed aliases should always be structural
|
||||
// (when does it make sense to give a host an opaque type?)
|
||||
let content = Content::Alias(
|
||||
|
@ -1232,7 +1691,7 @@ fn roc_result_to_var<'a>(
|
|||
) -> Variable {
|
||||
match result_type {
|
||||
Type::TagUnion(tags, ext) => {
|
||||
debug_assert!(ext.is_empty_tag_union());
|
||||
debug_assert!(ext.is_closed());
|
||||
debug_assert!(tags.len() == 2);
|
||||
|
||||
if let [(err, err_args), (ok, ok_args)] = &tags[..] {
|
||||
|
@ -1476,40 +1935,46 @@ fn type_to_union_tags<'a>(
|
|||
pools: &mut Pools,
|
||||
arena: &'_ bumpalo::Bump,
|
||||
tags: &'a [(TagName, Vec<Type>)],
|
||||
ext: &'a Type,
|
||||
ext: &'a TypeExtension,
|
||||
stack: &mut bumpalo::collections::Vec<'_, TypeToVar<'a>>,
|
||||
) -> (UnionTags, Variable) {
|
||||
use bumpalo::collections::Vec;
|
||||
|
||||
let sorted = tags.len() == 1 || sorted_no_duplicates(tags);
|
||||
|
||||
if ext.is_empty_tag_union() {
|
||||
let ext = Variable::EMPTY_TAG_UNION;
|
||||
match ext {
|
||||
TypeExtension::Closed => {
|
||||
let ext = Variable::EMPTY_TAG_UNION;
|
||||
|
||||
let union_tags = if sorted {
|
||||
insert_tags_fast_path(subs, rank, pools, arena, tags, stack)
|
||||
} else {
|
||||
let tag_vars = Vec::with_capacity_in(tags.len(), arena);
|
||||
insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack)
|
||||
};
|
||||
let union_tags = if sorted {
|
||||
insert_tags_fast_path(subs, rank, pools, arena, tags, stack)
|
||||
} else {
|
||||
let tag_vars = Vec::with_capacity_in(tags.len(), arena);
|
||||
insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack)
|
||||
};
|
||||
|
||||
(union_tags, ext)
|
||||
} else {
|
||||
let mut tag_vars = Vec::with_capacity_in(tags.len(), arena);
|
||||
(union_tags, ext)
|
||||
}
|
||||
TypeExtension::Open(ext) => {
|
||||
let mut tag_vars = Vec::with_capacity_in(tags.len(), arena);
|
||||
|
||||
let temp_ext_var = RegisterVariable::with_stack(subs, rank, pools, arena, ext, stack);
|
||||
let (it, ext) =
|
||||
roc_types::types::gather_tags_unsorted_iter(subs, UnionTags::default(), temp_ext_var);
|
||||
let temp_ext_var = RegisterVariable::with_stack(subs, rank, pools, arena, ext, stack);
|
||||
let (it, ext) = roc_types::types::gather_tags_unsorted_iter(
|
||||
subs,
|
||||
UnionTags::default(),
|
||||
temp_ext_var,
|
||||
);
|
||||
|
||||
tag_vars.extend(it.map(|(n, v)| (n.clone(), v)));
|
||||
tag_vars.extend(it.map(|(n, v)| (n.clone(), v)));
|
||||
|
||||
let union_tags = if tag_vars.is_empty() && sorted {
|
||||
insert_tags_fast_path(subs, rank, pools, arena, tags, stack)
|
||||
} else {
|
||||
insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack)
|
||||
};
|
||||
let union_tags = if tag_vars.is_empty() && sorted {
|
||||
insert_tags_fast_path(subs, rank, pools, arena, tags, stack)
|
||||
} else {
|
||||
insert_tags_slow_path(subs, rank, pools, arena, tags, tag_vars, stack)
|
||||
};
|
||||
|
||||
(union_tags, ext)
|
||||
(union_tags, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1821,7 +2286,7 @@ fn adjust_rank_content(
|
|||
Alias(_, args, real_var, _) => {
|
||||
let mut rank = Rank::toplevel();
|
||||
|
||||
for var_index in args.variables() {
|
||||
for var_index in args.all_variables() {
|
||||
let var = subs[var_index];
|
||||
rank = rank.max(adjust_rank(subs, young_mark, visit_mark, group_rank, var));
|
||||
}
|
||||
|
@ -1967,7 +2432,7 @@ fn instantiate_rigids_help(subs: &mut Subs, max_rank: Rank, initial: Variable) {
|
|||
let var = *var;
|
||||
let args = *args;
|
||||
|
||||
stack.extend(var_slice!(args.variables()));
|
||||
stack.extend(var_slice!(args.all_variables()));
|
||||
|
||||
stack.push(var);
|
||||
}
|
||||
|
@ -2216,7 +2681,9 @@ fn deep_copy_var_help(
|
|||
Alias(symbol, arguments, real_type_var, kind) => {
|
||||
let new_variables =
|
||||
SubsSlice::reserve_into_subs(subs, arguments.all_variables_len as _);
|
||||
for (target_index, var_index) in (new_variables.indices()).zip(arguments.variables()) {
|
||||
for (target_index, var_index) in
|
||||
(new_variables.indices()).zip(arguments.all_variables())
|
||||
{
|
||||
let var = subs[var_index];
|
||||
let copy_var = deep_copy_var_help(subs, max_rank, pools, visited, var);
|
||||
subs.variables[target_index] = copy_var;
|
||||
|
|
|
@ -10,7 +10,6 @@ mod helpers;
|
|||
#[cfg(test)]
|
||||
mod solve_expr {
|
||||
use crate::helpers::with_larger_debug_stack;
|
||||
use roc_collections::all::MutMap;
|
||||
use roc_types::pretty_print::{content_to_string, name_all_type_vars};
|
||||
|
||||
// HELPERS
|
||||
|
@ -47,7 +46,7 @@ mod solve_expr {
|
|||
module_src = &temp;
|
||||
}
|
||||
|
||||
let exposed_types = MutMap::default();
|
||||
let exposed_types = Default::default();
|
||||
let loaded = {
|
||||
let dir = tempdir()?;
|
||||
let filename = PathBuf::from("Test.roc");
|
||||
|
@ -5270,6 +5269,7 @@ mod solve_expr {
|
|||
toI32: Num.toI32,
|
||||
toI64: Num.toI64,
|
||||
toI128: Num.toI128,
|
||||
toNat: Num.toNat,
|
||||
toU8: Num.toU8,
|
||||
toU16: Num.toU16,
|
||||
toU32: Num.toU32,
|
||||
|
@ -5278,7 +5278,7 @@ mod solve_expr {
|
|||
}
|
||||
"#
|
||||
),
|
||||
r#"{ toI128 : Int * -> I128, toI16 : Int * -> I16, toI32 : Int * -> I32, toI64 : Int * -> I64, toI8 : Int * -> I8, toU128 : Int * -> U128, toU16 : Int * -> U16, toU32 : Int * -> U32, toU64 : Int * -> U64, toU8 : Int * -> U8 }"#,
|
||||
r#"{ toI128 : Int * -> I128, toI16 : Int * -> I16, toI32 : Int * -> I32, toI64 : Int * -> I64, toI8 : Int * -> I8, toNat : Int * -> Nat, toU128 : Int * -> U128, toU16 : Int * -> U16, toU32 : Int * -> U32, toU64 : Int * -> U64, toU8 : Int * -> U8 }"#,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -5580,4 +5580,57 @@ mod solve_expr {
|
|||
r#"[ A, B, C ]"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
// https://github.com/rtfeldman/roc/issues/2702
|
||||
fn tag_inclusion_behind_opaque() {
|
||||
infer_eq_without_problem(
|
||||
indoc!(
|
||||
r#"
|
||||
Outer k := [ Empty, Wrapped k ]
|
||||
|
||||
insert : Outer k, k -> Outer k
|
||||
insert = \m, var ->
|
||||
when m is
|
||||
$Outer Empty -> $Outer (Wrapped var)
|
||||
$Outer (Wrapped _) -> $Outer (Wrapped var)
|
||||
|
||||
insert
|
||||
"#
|
||||
),
|
||||
r#"Outer k, k -> Outer k"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_inclusion_behind_opaque_infer() {
|
||||
infer_eq_without_problem(
|
||||
indoc!(
|
||||
r#"
|
||||
Outer k := [ Empty, Wrapped k ]
|
||||
|
||||
when ($Outer Empty) is
|
||||
$Outer Empty -> $Outer (Wrapped "")
|
||||
$Outer (Wrapped k) -> $Outer (Wrapped k)
|
||||
"#
|
||||
),
|
||||
r#"Outer Str"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_inclusion_behind_opaque_infer_single_ctor() {
|
||||
infer_eq_without_problem(
|
||||
indoc!(
|
||||
r#"
|
||||
Outer := [ A, B ]
|
||||
|
||||
when ($Outer A) is
|
||||
$Outer A -> $Outer A
|
||||
$Outer B -> $Outer B
|
||||
"#
|
||||
),
|
||||
r#"Outer"#,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue